这是一个非常简单的系统,基于发送似乎存在安全漏洞的JSON消息。有一个Python服务器(使用标准库中包含的JSON模块)接收JSON对象并对其执行操作。如果它得到{"req": "ping"}
,它只是返回{"resp": "pong"}
。它还具有用于设置音量的命令,以及一个用于更改管理员密码的命令。管理员可以将任何 JSON 发送到此服务器。这是(server.py):
import json
import sys
def change_admin_password(p): pass # empty for test
def set_volume(v): pass # empty for test
def handle(js):
if (js["req"] == "ping"):
return {"resp": "pong"}
if (js["req"] == "change admin password"):
change_admin_password(js["args"]["password"])
return {"resp": "ok"}
if (js["req"] == "set volume"):
set_volume(int(js["args"]["volume"]))
return {"resp": "ok"}
print handle(json.load(sys.stdin))
它从 stdin 读取命令并对其进行处理。另一个脚本将从网络套接字读取 JSON 对象,并将其传递给此脚本。
其他用户必须使用 libjson 通过以 C++ 编写的代理。它只是阻止需要管理员权限的请求。例如,如果用户尝试更改管理员密码,代理将拒绝该命令:
$ echo '{"req": "change admin password", "args": {"password":"new"}}' | ./proxy
echo $?
1
这是代码(代理.cpp):
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
string req = n.at("req").as_string();
if (req == "change admin password") {
return 1;
}
cout << n.write();
}
要使用代理,管理套接字的主脚本将通过代理将数据通过管道传输到 Python 服务器:
$ echo '{"req": "ping"}' | ./proxy | python server.py
{'resp': 'pong'}
$ echo '{"req": "set volume", "args": {"volume": 50}}' | ./proxy | python server.py
{'resp': 'ok'}
如果用户尝试执行受限命令,它将按预期失败:
$ echo '{"req": "change admin password", "args": {"password": "new"}}' | ./proxy | python server.py
Traceback (most recent call last):
File "server.py", line 17, in <module>
[...]
但是出于某种原因,如果"req"键在JSON中两次(不应该是非法的吗?),非管理员用户可以更改管理员密码:
$ echo '{"req": "nothing", "req": "change admin password", "args": {"password": "new"}}' | ./proxy | python server.py
{'resp': 'ok'}
为什么?
尝试迈克麦克马洪的解决方法:
我尝试使用 JSONNode.find
作为解决方法,但它似乎不起作用。
我尝试只遍历所有元素:
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
for (JSONNode::json_iterator it = n.begin(); it != n.end(); it++) {
cout << "found one: " << it->at("x").as_string() << endl;
}
}
这有效:
$ g++ proxy.cpp libjson.a -o proxy && ./proxy
In file included from libjson.h:4:0,
from proxy.cpp:1:
_internal/Source/JSONDefs.h:157:6: warning: #warning , Release build of libjson, but NDEBUG is not on [-Wcpp]
{"a": {"x": 1}, "b": {"x": 2}}
found one: 1
found one: 2
除非如果 JSON 无效,它会出现段错误?我使用迭代器是错误的吗?
$ echo '{"a": {"x": 1}, {"b": {"x": 2}}' | ./proxy
found one: 1
Segmentation fault
我用n.find("y")
替换了n.begin()
:
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
for (JSONNode::json_iterator it = n.find("y"); it != n.end(); it++) {
cout << "found one: " << it->at("x").as_string() << endl;
}
}
它根本不起作用。我使用迭代器是错误的吗?
g++ proxy.cpp libjson.a -o proxy && ./proxy
In file included from libjson.h:4:0,
from proxy.cpp:1:
_internal/Source/JSONDefs.h:157:6: warning: #warning , Release build of libjson, but NDEBUG is not on [-Wcpp]
{"y": {"x": 1}}
Segmentation fault
解决方法的另一种尝试:
#include "libjson.h"
using namespace std;
int main() {
string json((istreambuf_iterator<char>(cin)), istreambuf_iterator<char>());
JSONNode n = libjson::parse(json);
for (JSONNode::json_iterator it = n.begin(); it != n.end(); it++) {
if (it->name() == "req" && it->as_string() == "change admin password")
{
cout << "found one " << endl;
}
}
}
它有效!
$ echo '{"req": "change admin password"}' | ./proxy
found one
$ echo '{"req": "x", "req": "change admin password"}' | ./proxy
found one
$ echo '{"req": "change admin password", "req": "x"}' | ./proxy
found one
但仍然存在无效 JSON 输入的段错误?
$ echo '{"req": "x", {"req": "x"}' | ./proxy
Segmentation fault
libjson http://sourceforge.net/p/libjson/bugs/47/中的已知错误
<小时 />考虑一下 JSON:
{ "same" : 4, "same": 5}
- 见 http://www.jslint.com/
如果输入 JSON,您将看到错误(重复的键名称)
- 另请参阅 http://jsonlint.com/
如果您输入 JSON,您将不会看到错误,但 JSON 将被重新格式化并删除第一个"相同"键。
简单看一下就会发现,标准说你"不应该"有重复的键名,而不是"不得",所以从技术上讲libjson是可以的。
您的API(即at(字符串),运算符)与这两个网站中的任何一个(如上)都不一致。
- JSLINT 显示错误
- jsonlint 杀死了第一个节点,让节点的值为 5,我认为这与 JavaScript 的覆盖行为一致。
libjson.at(字符串) 函数将返回 FIRST 条目(而不是最后一个)。
使用 JSONNode.find("req") 的解决方法
看查找方法
JSONNode.find("req");
返回指向节点数组的指针,该数组可用于确定是否已多次指定该数组。 虽然库应该覆盖后续条目之上的每个条目(并且根据有效的 JSON 不这样做是一个错误) - 您可以使用 find 方法来查找并获取迭代器以单步跳过匹配的节点。
JSONNode n = libjson::parse(json)
JSONNode::json_iterator it = n.find("req");
// iterate over the array
if (it[i].at("req").as_string() == "change admin password") {
return 1;
}
请注意,这不是世界上最好的解决方法,因为我想这会返回与参数匹配的所有节点,并且在复杂的结构中,可能有多个具有相同名称的节点嵌套在其他地方,满足您的需求,这应该足够了。
但是,如果 JSON 调用至关重要,则应查看另一个库来验证 JSON 调用,或者可能支持管理员可分辨的命令,例如管理请求的 areq
。这将允许您在代理上清除包含 areq 命令的任何请求,因为只有管理员才能生成此类请求(并且显然不会通过代理发送)。
任何包含管理命令的标准请求都会失败。
更新
尝试按原样使用迭代器,诚然C++不是我的主要语言,而且我不精通 STL 迭代器。
for (JSONNode::json_iterator jsonIter = n.begin(); jsonIter != n.end(); jsonIter++) {
if (jsonIter->name() == "req" &&
jsonIter->as_string() == "change admin password")
{
// found something do magic here
}
}
上述代码在我的测试中对我有用。
<小时 />无论是否违法,您都需要在代理和服务器中对此进行清理。 它滥用了大多数连接键的库中的可能漏洞(因为您不能有重复的键)。 查找它并将其清除到未被视为 JSON 对象的任何位置。
或者在您的代理之前放置另一层,该层获取 JSON 并通过解析器运行它 - 这将删除任何重复的键。
编辑
python中的JSON lib正在获取req
的第一个定义并应用其内容,然后在第二个定义req
上覆盖第一个定义并设置攻击者命令。
由于您的python应用程序盲目地假设任何传入的请求都已经过预先清理(坏主意),因此您需要在代理层对此进行清理,并可能考虑将API密钥用于管理员级别的请求,以便任何通过或绕过您的代理的内容仍然需要知道某种密钥。