130 lines
5.3 KiB
C
130 lines
5.3 KiB
C
/* test_priv.c - 提权模块里那几条"不能搞错"的不变量
|
|
*
|
|
* 这个模块摔过一跤: 上一版把认证路径也 setsid + 接管道, 于是 sudo 拿不到
|
|
* 控制终端, 真机上认证 100% 失败 (而替身 sudo 无条件成功, 给了个假绿).
|
|
* 所以这里把两条硬不变量钉住:
|
|
*
|
|
* 1. **提权调用里不能有 -n**. 带 -n 就等于"不许提问", 需要密码时直接失败,
|
|
* 而用户要的恰恰是"提权要有身份验证".
|
|
* 2. **没有控制终端时能识别出来**. 识别不出来就会起一个等不到输入的进程.
|
|
*/
|
|
#include "priv.h"
|
|
#include "util.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
static int failures = 0;
|
|
static int checks = 0;
|
|
|
|
#define CHECK(cond, ...) \
|
|
do { \
|
|
checks++; \
|
|
if (!(cond)) { \
|
|
failures++; \
|
|
printf(" ✗ %s:%d: ", __FILE__, __LINE__); \
|
|
printf(__VA_ARGS__); \
|
|
printf("\n"); \
|
|
} \
|
|
} while (0)
|
|
|
|
static int argv_has(char *const *a, const char *want) {
|
|
for (size_t i = 0; a[i]; i++)
|
|
if (strcmp(a[i], want) == 0)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
int main(void) {
|
|
puts("包装成 sudo -u <用户> -- <argv>");
|
|
/* 显式转换, 和 priv.c 里 w[0] = (char *)"sudo" 是同一个理由:
|
|
* priv_sudo_argv 的签名要 char *const[] (execvp 的要求), 而这里放进去的
|
|
* 字面量没有任何人会改写. 把整条链路改成非 const 才是真的丢 const 正确性.
|
|
*/
|
|
char *plain[] = {(char *)"/bin/sh", (char *)"-c", (char *)"rm -rf /tmp/x",
|
|
NULL};
|
|
char **w = priv_sudo_argv("root", plain);
|
|
CHECK(w != NULL, "应当返回数组");
|
|
CHECK(strcmp(w[0], "sudo") == 0, "第一个是 sudo, 实际 %s", w[0]);
|
|
CHECK(strcmp(w[1], "-u") == 0 && strcmp(w[2], "root") == 0,
|
|
"-u <用户> 紧跟其后");
|
|
CHECK(strcmp(w[3], "--") == 0,
|
|
"必须有 -- : 否则命令里的选项会被 sudo 当成自己的");
|
|
CHECK(strcmp(w[4], "/bin/sh") == 0 && strcmp(w[5], "-c") == 0 &&
|
|
strcmp(w[6], "rm -rf /tmp/x") == 0,
|
|
"原 argv 原样接在后面, 不被拆开");
|
|
CHECK(w[7] == NULL, "数组要 NULL 结尾");
|
|
/* 命门外的那条不变量 */
|
|
CHECK(!argv_has(w, "-n"),
|
|
"**不能带 -n**: 带 -n 就不许提问, 需要密码时直接失败 —— "
|
|
"而提权的要点就是让 sudo 能问密码");
|
|
CHECK(!argv_has(w, "-S"),
|
|
"也不能带 -S: 那要求把密码从 stdin 喂进去, 等于让 ligent 经手密码");
|
|
free(w);
|
|
|
|
puts("用户名为空时按 root");
|
|
w = priv_sudo_argv(NULL, plain);
|
|
CHECK(strcmp(w[2], "root") == 0, "空用户应落到 root, 实际 %s", w[2]);
|
|
free(w);
|
|
w = priv_sudo_argv("", plain);
|
|
CHECK(strcmp(w[2], "root") == 0, "空串也应落到 root");
|
|
free(w);
|
|
|
|
puts("sudo 的失败原话要换成能照做的中文");
|
|
char err[512];
|
|
memset(err, 0, sizeof err);
|
|
priv_explain(1, "sudo: no tty present and no askpass program specified",
|
|
err, sizeof err);
|
|
CHECK(strstr(err, "终端") != NULL, "缺终端要说终端, 实际 %s", err);
|
|
priv_explain(1, "Sorry, try again.", err, sizeof err);
|
|
CHECK(strstr(err, "认证") != NULL, "认证失败要说认证, 实际 %s", err);
|
|
priv_explain(1, "pluv is not in the sudoers file.", err, sizeof err);
|
|
CHECK(strstr(err, "sudo") != NULL && strstr(err, "权限") != NULL,
|
|
"没有 sudo 权限要说清楚, 实际 %s", err);
|
|
/* 认不出来就原样透传: 宁可给原文, 也不编一句可能不对的解释 */
|
|
priv_explain(1, "some brand new message", err, sizeof err);
|
|
CHECK(strstr(err, "some brand new message") != NULL,
|
|
"认不出来要透传原文, 实际 %s", err);
|
|
priv_explain(127, "无法执行 sudo: No such file or directory", err,
|
|
sizeof err);
|
|
CHECK(strstr(err, "没有 sudo") != NULL, "没有 sudo 时要说清楚, 实际 %s",
|
|
err);
|
|
|
|
puts("没有控制终端时能识别出来");
|
|
int fds[2];
|
|
if (pipe(fds) != 0) {
|
|
perror("pipe");
|
|
return 2;
|
|
}
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
perror("fork");
|
|
return 2;
|
|
}
|
|
if (pid == 0) {
|
|
close(fds[0]);
|
|
int rc = -1;
|
|
if (setsid() >= 0) { /* 摘掉控制终端 */
|
|
rc = priv_have_tty(); /* 必须是 0 */
|
|
}
|
|
ssize_t wr = write(fds[1], &rc, sizeof rc);
|
|
(void)wr;
|
|
close(fds[1]);
|
|
_exit(0);
|
|
}
|
|
close(fds[1]);
|
|
int got = -1;
|
|
ssize_t rd = read(fds[0], &got, sizeof got);
|
|
close(fds[0]);
|
|
while (waitpid(pid, NULL, 0) < 0) {
|
|
}
|
|
CHECK(rd == (ssize_t)sizeof got && got == 0,
|
|
"setsid() 之后不该认为有终端 (识别不出来就会卡住)");
|
|
|
|
printf("\n%d 项检查, %d 项失败\n", checks, failures);
|
|
return failures ? 1 : 0;
|
|
}
|