HackTheBox | Secret

文章描述了一次针对HackTheBox平台Secret靶机的渗透测试过程,涉及了nmap端口扫描、源码下载、git日志分析以获取token,以及通过Node.js应用的逻辑漏洞进行权限提升,最终通过生成coredump文件并解析获取root权限的过程。
摘要由CSDN通过智能技术生成

HackTheBox | Secret

nmap扫描,开放22、80、3000;其中3000端口为Node.js的站点

image-20230105191130677

访问80端口跟3000端口,都是如下页面

image-20230105232921348

可以下载源码

image-20230105232935558

下载并解压源码,看到.git目录

image-20230105233035266

使用git log命令查看git提交日志,看到removed .env for security reasons

image-20230105233133569

先查看当前的.env文件

image-20230105233425939

执行git show 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78查看该次commit的具体提交,获取到一个token

gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE

image-20230105233822750

在最后一次commit中提到了可以view logs from server,所以也检查一下该次的具体提交内容

由于不太懂node.js,所以先放着

commit e297a2797a5f62b6011654cf6fb6ccb6712d2d5b (HEAD -> master)
Author: dasithsv <dasithsv@gmail.com>
Date:   Thu Sep 9 00:03:27 2021 +0530

    now we can view logs from server 😃

diff --git a/routes/private.js b/routes/private.js
index 1347e8c..cf6bf21 100644
--- a/routes/private.js
+++ b/routes/private.js
@@ -11,10 +11,10 @@ router.get('/priv', verifytoken, (req, res) => {

     if (name == 'theadmin'){
         res.json({
-            role:{
-
-                role:"you are admin",
-                desc : "{flag will be here}"
+            creds:{
+                role:"admin",
+                username:"theadmin",
+                desc : "welcome back admin,"
             }
         })
     }
@@ -26,7 +26,32 @@ router.get('/priv', verifytoken, (req, res) => {
             }
         })
     }
+})
+

+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) {
@@ -40,4 +65,4 @@ router.use(function (req, res, next) {
 });


-module.exports = router
\ No newline at end of file
+module.exports = router

查看源码,index.js中可以了解基本逻辑

const express = require('express');
const app = express();
const mongoose = require('mongoose');
const dotenv = require('dotenv')
const privRoute = require('./routes/private')
const bodyParser = require('body-parser')

app.use(express.static('public'))
app.use('/assets', express.static(__dirname + 'public/assets'))
app.use('/download', express.static(__dirname + 'public/source'))

app.set('views', './src/views')
app.set('view engine', 'ejs')


// 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)

app.listen(3000, () => console.log("server up and running"));

routes文件夹下找到private.js,发现和前面git show最后一次commit的内容基本一致;可以向/priv或者/logs页面提交请求,对于提交的内容会调用verifytoken

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

verifytoken中看到主要使用jwt.verify方法进行验证,所以需要向指定页面/priv/logs发送包含jwt token的请求

# verifytoken
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");
    }
};

auth.js中看到jwt的格式,payload部分需要包含_idnameemail

image-20230106000557418

使用在线网站https://jwt.io/,生成jwt token。根据之前对private.js代码的分析,可以知道用户名为theadmin,没有对id和email进行验证,所以先随便写;在signature部分,将之前在.env获取到的TOKEN写到第三个字段

image-20230106001130653

使用BP抓包,发送数据

image-20230106001412799

对一些关键字段进行解释

  • /api/priv:在index.js中其实只写了三条路由,分别是到authRoute->/api/userprivRoute->/apiwebRoute->/,看常量定义和路径,privRoute对应routes中的private.js,结合private.js中的两条,所以请求/api/priv

  • auth-token:在auth.js中其实可以看到res.header('auth-token', token).send(token);,所以在HTTP请求中字段名为auth-token

请求/api/logs,同样成功通过认证

image-20230106001859070

在logs的函数部分,提到了参数file,用于git log命令拼接,所以可以尝试命令截断执行

image-20230106002107798

反弹shell

image-20230106002857625

image-20230106002840526

bd6a3f9c29090aacf16faa43412053a9

由于不知道dasith用户的密码,所以无法执行sudo -l命令检查sudo特权

image-20230106090238418

检查SUID置位的文件,发现特殊文件/opt/count

image-20230106090335217

同目录下存在code.cvalgrind.log,检查三个文件的时间,也比较相近。valgrind.log可能是count的执行历史记录

image-20230106090703470

code.c可能是源码文件

#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;
}

在main函数中,看到Enable coredump generation,对应代码prctl(PR_SET_DUMPABLE, 1);

    // drop privs to limit file write
    setuid(getuid());
    // Enable coredump generation
    prctl(PR_SET_DUMPABLE, 1);

利用搜索引擎,大概了解到这两行代码是为了生成一个内存的状态文件,参考https://www.cnblogs.com/hazir/p/linxu_core_dump.html

image-20230106091849827

尝试生成core dump文件;运行./count,输入文件名/etc/shadow

image-20230106094923755

另开一个shell接收新的反弹shell,在该shell中查找count进程,然后发送SIGSEGV信号,在count的执行页面能够看到Segmentation fault

image-20230106095049894

但是没有找到core dump文件。在网上看到的说一般会在可执行程序的同一个目录下,或者/usr/share/apport/路径下,但是都没有看到。

在wp中看到存储于/var/crash/路径下

image-20230106095305112

根据wp的提示,在主机上找到了apport-unpack工具

image-20230106100413003

unpack生成的crash文件

image-20230106100627350

查看CoreDump可以获取到一些文件内容

image-20230106100700550

同样可以获取到root用户的私钥文件;注意,此处生成crash文件时,需要先将原来的crash文件删除

image-20230106100842085

获取到root的SSH私钥

image-20230106101311889

将ssh私钥保存到文件中,并将文件权限修改为600,使用ssh私钥登录,拿到root权限

image-20230106101517199

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值