I have written a small c++ program to connect to my router (avm Fritzbox 7590) using a challenge response method.
For this I use openssl (SHA256), curl(GET & POST) and tinyXML (parsing the response).
I have a member function "query_challenge()" to query the FritzBox for a challenge.
This I receive as .xml, this I receive:
<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
<SID>0000000000000000</SID>
<Challenge>2$60000$*******************$6000$15a84421a7dfc4d9ac8026f776de48f3</Challenge>
<BlockTime>0</BlockTime>
<Rights/>
<Users>
<User last="1">adminxxx</User>
</Users>
</SessionInfo>
With the member function "std::string calculate_pbkdf2_response()" I evaluate the received challenge and send this "query_sessionID()" with a POST to the FritzBox to log in and get a valid SessionID.
When calling my query_sessionID() method, if everything is correct, I should get another XML via the POST in which a valid SID(SessionID) is contained.
However, I get the following response:
<?xml version="1.0" encoding="utf-8"?>
<SessionInfo>
<SID>0000000000000000</SID>
<Challenge>2$60000$diffent*******************$6000$1268d1df004ffdb2fd8074b557467a01</Challenge>
<BlockTime>128</BlockTime>
<Rights/>
<Users>
<User last="1">adminxxx</User>
</Users>
</SessionInfo>
which shows that either the calculated resopnse is incorrect or the POST may not have worked correctly.
I tested my calculate_pbkdf2_response() with the example below.
The example challenge “2$10000$5A1711$2000$5A1722” and the password “1example!“ (utf8-encoded) results in: hash1 = pbdkf2_hmac_sha256(“1example!”, 5A1711, 10000) => 0x23428e9dec39d95ac7a40514062df0f9e94f996e17c398c79898d0403b332d3b (hex) response = 5A1722$ + pbdkf2_hmac_sha256(hash1, 5A1722, 2000).hex() => 5A1722$1798a1672bca7c6463d6b245f82b53703b0f50813401b03e4045a5861e689adb https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf
This "5A1722$1798a1672bca7c6463d6b245f82b53703b0f50813401b03e4045a5861e689adb" response gets my calculate_pbkdf2_response() method also out with the respective PW and challenge.
So I suspect that my CURL POST request may not be correct. Maybe someone has a tip who has more experience with this stuff, this is my first http attempt with CURL as well as the first use of openSSL or challenge response method.
main.cpp:
#include <iostream>
#include "../public/FritzBox_Session.hpp"
int main(int argc, char const *argv[])
{
std::string user = "adminxxx";
std::string pw = "passwort";
homeAutomat::FritzBox::FritzBox_Session TEST(user, pw);
TEST.establish_connection();
return 0;
}
FritzBox_Session.hpp:
#include <iostream>
#include <string>
#include <sstream>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <tinyxml2.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
namespace homeAutomat {
namespace FritzBox {
class FritzBox_Session
{
private:
CURL *curl;
const std::string fritzbox_addr = "http://fritz.box/login_sid.lua?version=2";
std::string mUsername;
std::string mPassword;
std::stringstream mChallenge;
std::string mSID;
tinyxml2::XMLDocument XML_response;
bool query_challenge();
bool query_sessionID();
std::string calculate_pbkdf2_response();
public:
FritzBox_Session(std::string &username, std::string &password);
~FritzBox_Session();
bool establish_connection();
};
} // FritzBox
} // homeAutomat
FritzBox_Session.cpp:
#include "FritzBox_Session.hpp"
using namespace homeAutomat;
using namespace FritzBox;
using namespace nlohmann;
namespace {
// CURL - ReadReceivedData Callback-Fuction
size_t writeFunction(void *ptr, size_t size, size_t nmemb, void *userdata)
{
((std::string*)userdata)->append((char*)ptr, size * nmemb);
return size * nmemb;
}
}
FritzBox_Session::FritzBox_Session(std::string &username, std::string &password)
:
mUsername(username),
mPassword(password)
{
curl = curl_easy_init();
if(!curl){
std::cerr << "curl isn't initialized! " << std::endl;
}
}
FritzBox_Session::~FritzBox_Session()
{
curl_easy_cleanup(curl);
}
bool FritzBox_Session::establish_connection()
{
if(query_challenge()){
if(query_sessionID()){
}
else{
std::cerr << "error at query_sessionID() occurred" << std::endl;
return false;
}
}
else{
std::cerr << "error at query_challenge() occurred" << std::endl;
return false;
}
return true;
}
bool FritzBox_Session::query_challenge()
{
std::string readBuffer;
CURLcode retVal;
if(!curl){
std::cerr << "curl isn't initialized!'" << std::endl;
return 1;
}
curl_easy_setopt(curl, CURLOPT_URL, fritzbox_addr.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
retVal = curl_easy_perform(curl);
if(retVal != CURLE_OK){
std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(retVal) << std::endl;
return false;
}
int error_id = XML_response.Parse(&readBuffer[0]);
if(error_id != tinyxml2::XML_SUCCESS){
std::cout << "xml parse error" << XML_response.ErrorName() << std::endl;
XML_response.Clear();
return false;
}
auto save_xml_to_file = XML_response.SaveFile("response_query_challenge.xml");
if(save_xml_to_file != tinyxml2::XML_SUCCESS){
std::cout << "xml save to file error: " << XML_response.ErrorName() << std::endl;
}
// search for <SessionInfo>
tinyxml2::XMLElement* sessionInfo = XML_response.FirstChildElement("SessionInfo");
if(sessionInfo == nullptr){
std::cerr << "XML response has no <SessionInfo>-Variable!" << std::endl;
XML_response.Clear();
return false;
}
// search for <SessionInfo> <Challenge> </SessionInfo>
tinyxml2::XMLElement* challenge = sessionInfo->FirstChildElement("Challenge");
if(challenge == nullptr){
std::cerr << "XML response has no <Challenge>-Variable!" << std::endl;
XML_response.Clear();
return false;
}
// get String Value of <SessionInfo> <Challenge> </SessionInfo>
mChallenge << challenge->GetText();
if(mChallenge.str().empty()){
std::cerr << "XML response <Challenge> is empty!" << std::endl;
XML_response.Clear();
return false;
}
XML_response.Clear();
return true;
}
bool FritzBox_Session::query_sessionID()
{
CURLcode retVal;
std::string readBuffer;
std::string response = calculate_pbkdf2_response();
if (!curl) {
std::cerr << "Fehler beim Initialisieren von CURL" << std::endl;
return 1;
}
// HTTP Method 'POST'
curl_easy_setopt(curl, CURLOPT_POST, 1L);
// Set POST Data
std::string post_data = "username=" + mUsername + "&response=" + response;
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_data.length());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, &post_data[0]);
// Set Content-Type-Header
struct curl_slist* headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// Set Callback to writeFunction() received data
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
// Send POST data and received answer
retVal = curl_easy_perform(curl);
if(retVal != CURLE_OK){
std::cerr << "Error while sending the request: " << curl_easy_strerror(retVal) << std::endl;
return false;
}
else{
int error_id = XML_response.Parse(&readBuffer[0]);
if(error_id != tinyxml2::XML_SUCCESS){
std::cout << "xml parse error" << XML_response.ErrorName() << std::endl;
}
auto save_xml_to_file = XML_response.SaveFile("response_query_sessionID.xml");
if(save_xml_to_file != tinyxml2::XML_SUCCESS){
std::cout << "xml save to file error: " << XML_response.ErrorName() << std::endl;
}
tinyxml2::XMLElement* sessionInfo = XML_response.FirstChildElement("SessionInfo");
if(sessionInfo == nullptr){
std::cerr << "XML response has no <SessionInfo>-Variable!" << std::endl;
XML_response.Clear();
return false;
}
tinyxml2::XMLElement* SID = sessionInfo->FirstChildElement("SID");
if(SID == nullptr){
std::cerr << "XML response has no <SID>-Variable!" << std::endl;
XML_response.Clear();
return false;
}
mSID = SID->GetText();
if(mSID.empty()){
std::cerr << "XML response <SID> is empty!" << std::endl;
XML_response.Clear();
return false;
}
if(mSID.compare("0000000000000000") == 0){
std::cerr << "username or password wrong!" << std::endl;
}
else{
std::cout << "login was successful SID is: " << mSID << std::endl;
}
return true;
}
}
std::string FritzBox_Session::calculate_pbkdf2_response()
{
// Split the challenge into parts
std::vector<std::string> challenge_parts;
std::string item;
while (std::getline(mChallenge, item, '$'))
{
challenge_parts.push_back(item);
}
// Extract all necessary values encoded into the challenge
int iter1 = std::stoi(challenge_parts[1]);
int iter2 = std::stoi(challenge_parts[3]);
// Convert the salt strings to unsigned char arrays
unsigned char salt1[challenge_parts[2].size() / 2];
unsigned char salt2[challenge_parts[4].size() / 2];
for (int i = 0; i < challenge_parts[2].size(); i += 2) {
sscanf(challenge_parts[2].substr(i, 2).c_str(), "%02x", &salt1[i / 2]);
}
for (int i = 0; i < challenge_parts[4].size(); i += 2) {
sscanf(challenge_parts[4].substr(i, 2).c_str(), "%02x", &salt2[i / 2]);
}
// Hash twice, once with static salt...
unsigned char hash1[SHA256_DIGEST_LENGTH];
PKCS5_PBKDF2_HMAC(mPassword.c_str(), mPassword.size(), salt1, sizeof(salt1), iter1, EVP_sha256(), SHA256_DIGEST_LENGTH, hash1);
// Once with dynamic salt.
unsigned char hash2[SHA256_DIGEST_LENGTH];
PKCS5_PBKDF2_HMAC((const char*)hash1, sizeof(hash1), salt2, sizeof(salt2), iter2, EVP_sha256(), SHA256_DIGEST_LENGTH, hash2);
// Convert the hash2 array to a hexadecimal string
char hash2_hex[SHA256_DIGEST_LENGTH * 2 + 1];
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
sprintf(hash2_hex + i * 2, "%02x", hash2[i]);
}
return challenge_parts[4] + '$' + hash2_hex;
}