HackTheBox | Secret
nmap扫描,开放22、80、3000;其中3000端口为Node.js的站点
访问80端口跟3000端口,都是如下页面
可以下载源码
下载并解压源码,看到.git
目录
使用git log
命令查看git提交日志,看到removed .env for security reasons
先查看当前的.env
文件
执行git show 67d8da7a0e53d8fadeb6b36396d86cdcd4f6ec78
查看该次commit的具体提交,获取到一个token
gXr67TtoQL8TShUc8XYsK2HvsBYfyQSFCFZe4MQp7gRpFuMkKjcM72CNQN4fMfbZEKx4i7YiWuNAkmuTcdEriCMm9vPAYkhpwPTiuVwVhvwE
在最后一次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部分需要包含_id
、name
、email
使用在线网站https://jwt.io/
,生成jwt token。根据之前对private.js
代码的分析,可以知道用户名为theadmin
,没有对id和email进行验证,所以先随便写;在signature部分,将之前在.env
获取到的TOKEN写到第三个字段
使用BP抓包,发送数据
对一些关键字段进行解释
-
/api/priv
:在index.js中其实只写了三条路由,分别是到authRoute->/api/user
、privRoute->/api
和webRoute->/
,看常量定义和路径,privRoute对应routes中的private.js
,结合private.js中的两条,所以请求/api/priv
-
auth-token
:在auth.js
中其实可以看到res.header('auth-token', token).send(token);
,所以在HTTP请求中字段名为auth-token
请求/api/logs
,同样成功通过认证
在logs的函数部分,提到了参数file
,用于git log
命令拼接,所以可以尝试命令截断执行
反弹shell
bd6a3f9c29090aacf16faa43412053a9
由于不知道dasith用户的密码,所以无法执行sudo -l
命令检查sudo特权
检查SUID
置位的文件,发现特殊文件/opt/count
同目录下存在code.c
和valgrind.log
,检查三个文件的时间,也比较相近。valgrind.log可能是count的执行历史记录
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
尝试生成core dump文件;运行./count
,输入文件名/etc/shadow
另开一个shell接收新的反弹shell,在该shell中查找count进程,然后发送SIGSEGV
信号,在count的执行页面能够看到Segmentation fault
但是没有找到core dump文件。在网上看到的说一般会在可执行程序的同一个目录下,或者/usr/share/apport/路径下,但是都没有看到。
在wp中看到存储于/var/crash/
路径下
根据wp的提示,在主机上找到了apport-unpack
工具
unpack生成的crash文件
查看CoreDump
可以获取到一些文件内容
同样可以获取到root用户的私钥文件;注意,此处生成crash文件时,需要先将原来的crash文件删除
获取到root的SSH私钥
将ssh私钥保存到文件中,并将文件权限修改为600
,使用ssh私钥登录,拿到root权限